commit 9227922e4d67f6df3c79f783d5b3bd5f94acd5cf parent 23e96a601e150ab79c6329246a0523d972a625e6 Author: James Eversole <james@eversole.co> Date: Sun, 25 Oct 2026 08:04:25 -0500 Refresh playground examples and remove runtime selector Diffstat:
45 files changed, 439 insertions(+), 270 deletions(-)
diff --git a/ext/demo-site/README.md b/ext/demo-site/README.md @@ -82,12 +82,12 @@ The pinned editor sources, fixed-output hashes, licenses and declaration build are in `codemirror/`. Nix builds and bundles the editor locally. Physical mobile Safari, real IME and screen-reader behavior require device/manual validation. -## Runtime selection +## Runtime The playground uses the canonical Haskell evaluator compiled to WebAssembly. -The Runtime control identifies **Haskell**; invalid backend selections reject -without evaluator fallback. Restarting a runtime preserves page-owned source and -local modules and recompiles drafts. Stop terminates the owning Worker tree. +Stop terminates the owning Worker tree. Compiling again starts a fresh runtime +from the page-owned source and local modules. Missing or invalid runtime assets +produce an error. The independent JavaScript graph, codec and compiler conformance tools live under `src/conformance/` (generated into `dist/conformance/`) and are excluded from the @@ -199,8 +199,11 @@ The selector starts with **Basic Examples**, in learning order: - **Transforming a list**: pass a function to `map`, then total the results. - **Making a choice**: ordered pattern equations and a fallback message. -Each lesson supplies example applications, interpretations and expected results, -followed by a small edit exercise. **Practical Examples** contains the application +Lessons introduce syntax in source comments and invite small edits. Click or tap +underlined definition names to inspect their values. To try a function call, add +a named result definition; edits compile after a pause. The basic lessons include +named results alongside their functions and use open imports from base for +arithmetic and list operations. **Practical Examples** contains the application examples below. **Language & Runtime Explorations** starts with **Language reference**, an independent syntax sampler covering definitions, lambdas, partial application, literals, patterns, captures, pins, local bindings, @@ -416,7 +419,7 @@ or another edit rebuilds them; Stop does not automatically restart work. A stopp observation requires another click on its definition name after rebuilding. Published sessions offer **Reload package**, which retries the same selection without a full page reload; a failed first load must not turn an editable demo into a source-free package. -Changing backend rebuilds a draft from its source. Sessions remain active while +After Stop, compiling a draft rebuilds its runtime from source. Sessions remain active while scrolling through the page. An elapsed compilation indicator and Stop remain available. @@ -1468,8 +1471,7 @@ opened local module triggers a confirmation before navigation; cancelling leaves that work and the current operation untouched. Copy anything needed before confirming: drafts, inspection results, IO state and the in-memory local-module collection are not restored. Arrival follows the normal URL/default selection rules and -never automatically evaluates a program. The runtime selector returns to its -normal page default. This is a recovery path, not a diagnosis or prevention of +never automatically evaluates a program. This is a recovery path, not a diagnosis or prevention of long-idle runtime/browser failures. Add `TEST_RELOAD=1` to a Chromium backend harness invocation to exercise warning diff --git a/ext/demo-site/bundles/4116a3702923898d5fafc8c855fbea38a7fd740bdb15667ef53fec7268b41381.arboricx b/ext/demo-site/bundles/4116a3702923898d5fafc8c855fbea38a7fd740bdb15667ef53fec7268b41381.arboricx Binary files differ. diff --git a/ext/demo-site/bundles/639c8d3959320ef99d9416257905f189519a90ea4b315cdbb0485a386e46191c.arboricx b/ext/demo-site/bundles/639c8d3959320ef99d9416257905f189519a90ea4b315cdbb0485a386e46191c.arboricx Binary files differ. diff --git a/ext/demo-site/bundles/6c85d85a51c0c3ed8fb9adc1a5c81c18313174b3f72371933e432f3f895c1667.arboricx b/ext/demo-site/bundles/6c85d85a51c0c3ed8fb9adc1a5c81c18313174b3f72371933e432f3f895c1667.arboricx Binary files differ. diff --git a/ext/demo-site/bundles/8eb72ff6766fb762614c6dca9026ebda78d06f5e8f78e48cc29b195ba2154b21.arboricx b/ext/demo-site/bundles/8eb72ff6766fb762614c6dca9026ebda78d06f5e8f78e48cc29b195ba2154b21.arboricx Binary files differ. diff --git a/ext/demo-site/bundles/ac504497f656b84b7ae1e7f9a00086c76d05686df3eee2c5612a8c7f51fbf141.arboricx b/ext/demo-site/bundles/ac504497f656b84b7ae1e7f9a00086c76d05686df3eee2c5612a8c7f51fbf141.arboricx Binary files differ. diff --git a/ext/demo-site/bundles/dd0931401769303bf2e1ce08c51e7778779ccc9643ca8315c256c0542eedff7a.arboricx b/ext/demo-site/bundles/dd0931401769303bf2e1ce08c51e7778779ccc9643ca8315c256c0542eedff7a.arboricx Binary files differ. diff --git a/ext/demo-site/catalogue.json b/ext/demo-site/catalogue.json @@ -3,9 +3,9 @@ "id": "names-values", "title": "Names and values", "category": "Basic Examples", - "hash": "8d1b7e1f6dfefe983167ee8d9631476caaa0c3dc689a85628e9ca627ebaef6d0", - "source": "!export greeting\n!export answer\n\n-- Give a value a name.\ngreeting = \"Hello, world!\"\n-- Evaluate greeting with Text.\n-- Result: Hello, world!\n-- Try changing the words, then Evaluate again.\n\n-- Numbers can have names too.\nanswer = 42\n-- Choose answer in Definition.\n-- It selects Natural number; the result is 42.\n\n-- Exercise: change answer to your favourite number and evaluate it.\n-- The !export lines make our names available to the playground.\n-- Edits compile after a pause; Evaluate runs the expression.\n-- Next: Doing arithmetic.\n", - "sourceSha256": "77d8c61538436b86f620612a8502ba2980838b6e370cf71f6a364ff851517f3e", + "hash": "4116a3702923898d5fafc8c855fbea38a7fd740bdb15667ef53fec7268b41381", + "source": "!export *\n\n-- Welcome to tricu! Anything after -- on a line is a comment.\n-- tricu is a lazy functional language based on tree calculus.\n\n-- Click or tap underlined definition names to inspect their values.\n-- Function calls can have names too; we'll try that in \"Your first function\".\n-- Edits compile after a pause. IO programs offer \"Run IO\" inside their card.\n\n-- tricu offers string literals,\ngreeting = \"Hello, world!\"\n\n-- natural number literals (whole numbers starting at zero),\nanswer = 42\n\n-- list literals,\ngreetAndAnswer = [greeting answer]\n\n-- and more! Choose \"Doing arithmetic\" from the \"Program\" dropdown next.\n", + "sourceSha256": "f91426877f7cbd74aa4de3f8241844e38bd724ed1c3766ee50e04f548a6a31fd", "export": "greeting", "arguments": [], "output": "string", @@ -20,6 +20,11 @@ "arguments": [], "output": "number", "mode": "pure" + }, + "greetAndAnswer": { + "arguments": [], + "output": "values", + "mode": "pure" } }, "environment": "empty" @@ -28,9 +33,9 @@ "id": "arithmetic", "title": "Doing arithmetic", "category": "Basic Examples", - "hash": "38110ab45a75a9c8771d6422277cb3083e667a4368aa33df0b43af672f17375e", - "source": "!import 7f87549d587fa8c3be88af0ea988db3adcc3b35f1ffb48973553491b075614db as Math\n!export *\n\n-- Add the prices of two items.\ntotal = Math.add 5 7\n-- Evaluate total with Natural number.\n-- Result: 12.\n-- Add import supplies the library identity; Math is our short name.\n\n-- Function calls use spaces: Math.multiply takes two numbers.\nproduct = Math.multiply 5 7\n\n-- Parentheses make one calculation the input to another.\ngrouped = Math.multiply (Math.add 5 7) 2\n\n-- Try product (35), then grouped (24), with Natural number.\n-- Exercise: change total to add the prices of two things you would buy.\n-- Numbers here are whole numbers starting at zero.\n-- Next: Your first function.\n", - "sourceSha256": "67bfda56d700499e9f484f73d670c22156842fc963798127284be1f8482e2ba8", + "hash": "ac504497f656b84b7ae1e7f9a00086c76d05686df3eee2c5612a8c7f51fbf141", + "source": "!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 *\n!export *\n\n-- Let's do some arithmetic! Our import supplies add and mul (multiply).\n-- Function calls use spaces between the function and its inputs.\ntotal = add 5 7\nmultiplied = mul 5 7\n\n-- Parentheses make one calculation the input to another.\ngrouped = mul (add 5 7) 2\n\n-- Try changing the numbers to calculate the cost of two things you'd buy.\n-- Next: Your first function.\n", + "sourceSha256": "de1aea17cb2cab0a386f491f1e08fe00aec48c80967740ee55756987c69d328d", "export": "total", "arguments": [], "output": "number", @@ -41,7 +46,7 @@ "output": "number", "mode": "pure" }, - "product": { + "multiplied": { "arguments": [], "output": "number", "mode": "pure" @@ -58,19 +63,24 @@ "id": "functions", "title": "Your first function", "category": "Basic Examples", - "hash": "be5f4675c9b0b6d9c093fe44a9695da9f428c88d62b0f4f1340017067fa81fa9", - "source": "!import 7f87549d587fa8c3be88af0ea988db3adcc3b35f1ffb48973553491b075614db as Math\n!export *\n\n-- Reuse a calculation with different inputs.\ndouble number = Math.multiply number 2\n-- Evaluate double 6 with Natural number.\n-- Result: 12.\n-- Add import supplies Math, including multiplication.\n\n-- number is a parameter: each call supplies its value.\n-- Try double 9: the same definition now returns 18.\n\n-- A function can also take more than one input.\nadd left right = Math.add left right\n-- Try add 4 7 with Natural number: the result is 11.\n\n-- Exercise: change double to multiply by 3, and give it the name triple.\n-- Then evaluate triple 6. Update the expression to use your new name.\n-- Next: Working with lists.\n", - "sourceSha256": "44560b2f0d1df0ef24b853cc0e9a31a9a57108d8cfa56653a1840d264e498a24", - "export": "double", - "arguments": [ - { - "type": "number", - "value": "6" - } - ], + "hash": "6c85d85a51c0c3ed8fb9adc1a5c81c18313174b3f72371933e432f3f895c1667", + "source": "!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 *\n!export *\n\n-- Give a calculation a name so we can reuse it with different inputs.\ndouble number = mul number 2\n\n-- number is a parameter: each call supplies its value.\nsixDoubled = double 6\nnineDoubled = double 9\n\n-- A function can take more than one input.\nrectangle width height = mul width height\narea = rectangle 4 7\n\n-- Try writing triple and using it in place of double.\n-- Next: Working with lists.\n", + "sourceSha256": "3e07457439457ad90533a9e1ecee410d3648d001b7308c6e8aaf41f03644e2f7", + "export": "sixDoubled", + "arguments": [], "output": "number", "mode": "pure", "presets": { + "sixDoubled": { + "arguments": [], + "output": "number", + "mode": "pure" + }, + "nineDoubled": { + "arguments": [], + "output": "number", + "mode": "pure" + }, "double": { "arguments": [ { @@ -81,7 +91,7 @@ "output": "number", "mode": "pure" }, - "add": { + "rectangle": { "arguments": [ { "type": "number", @@ -94,6 +104,11 @@ ], "output": "number", "mode": "pure" + }, + "area": { + "arguments": [], + "output": "number", + "mode": "pure" } }, "environment": "empty" @@ -102,15 +117,15 @@ "id": "lists", "title": "Working with lists", "category": "Basic Examples", - "hash": "3cc5d36c5b70a20362117e3349aa044efca8adca56105424965065395a9f3e76", - "source": "!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 as List\n!export count\n!export total\n\n-- Collect values in a list.\nnumbers = [2 4 6]\ncount = List.length numbers\ntotal = List.sum numbers\n\n-- Evaluate count with Natural number: the result is 3.\n-- Spaces separate items. [] is the empty list.\n-- Add import supplies List, including length and sum.\n\n-- Try total with Natural number: the result is 12.\n-- Exercise: add 8 to numbers. Check count (4) and total (20).\n-- Then try numbers = []: both results become 0.\n-- Next: Transforming a list.\n", - "sourceSha256": "8d1dcc3840ccbfff3e2b64de18ba96ea81346b317dded4b7d3414657acf4fa7f", - "export": "count", + "hash": "8eb72ff6766fb762614c6dca9026ebda78d06f5e8f78e48cc29b195ba2154b21", + "source": "!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 *\n!export *\n\n-- Collect values in a list. Spaces separate items; [] is the empty list.\nnumbers = [2 4 6]\nitemCount = length numbers\ntotal = sum numbers\n\n-- Try adding another item to numbers, then try numbers = [].\n-- Next: Transforming a list.\n", + "sourceSha256": "ba33f8097d479eeb3adcbcc92c7b33931970df2b7ea4d523535673a095e5a1b2", + "export": "itemCount", "arguments": [], "output": "number", "mode": "pure", "presets": { - "count": { + "itemCount": { "arguments": [], "output": "number", "mode": "pure" @@ -119,6 +134,11 @@ "arguments": [], "output": "number", "mode": "pure" + }, + "numbers": { + "arguments": [], + "output": "values", + "mode": "pure" } }, "environment": "empty" @@ -127,9 +147,9 @@ "id": "transform-list", "title": "Transforming a list", "category": "Basic Examples", - "hash": "9f10d772b42628acfe843eec7524d8de1ba9ea4344d3952c6c091c98c4d1c72d", - "source": "!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 as List\n!import 7f87549d587fa8c3be88af0ea988db3adcc3b35f1ffb48973553491b075614db as Math\n!export total\n!export double\n\n-- Apply the same function to every item.\ndouble number = Math.multiply number 2\nnumbers = [2 4 6]\n\n-- map takes a function, then a list. Here it produces [4 8 12].\ndoubled = List.map double numbers\ntotal = List.sum doubled\n-- Evaluate total with Natural number: the result is 24.\n-- Add import supplies List and Math.\n\n-- Try double 5 with Natural number: the result is 10.\n-- Exercise: make double multiply by 3. The new total should be 36.\n-- Next: Making a choice.\n", - "sourceSha256": "8e33d918846404760b398b473a93effad390b1486d5c2b053078170226cb8da9", + "hash": "639c8d3959320ef99d9416257905f189519a90ea4b315cdbb0485a386e46191c", + "source": "!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 *\n!export *\n\n-- Functions are values too. We can pass one to another function.\ndouble number = mul number 2\nnumbers = [2 4 6]\n\n-- map takes a function, then a list, and applies it to every item.\ndoubled = map double numbers\n-- map builds a new list; the original numbers are unchanged.\ntotal = sum doubled\n\n-- Try multiplying by 3 instead.\n-- Next: Making a choice.\n", + "sourceSha256": "e752330fcedfc61065c695ebbafa1e6bc65745d890ee98c7682d4e5fe11bc45f", "export": "total", "arguments": [], "output": "number", @@ -149,6 +169,16 @@ ], "output": "number", "mode": "pure" + }, + "numbers": { + "arguments": [], + "output": "values", + "mode": "pure" + }, + "doubled": { + "arguments": [], + "output": "values", + "mode": "pure" } }, "environment": "empty" @@ -157,19 +187,29 @@ "id": "choices", "title": "Making a choice", "category": "Basic Examples", - "hash": "1972c98bfe599fef2a72cc946401a5cc9280026a8c46fdd7c8ccdc7093ee90cc", - "source": "!export message\n\n-- Choose an answer by matching an input.\nmessage 0 = \"Your basket is empty.\"\nmessage 1 = \"You have one item.\"\nmessage _ = \"You have several items.\"\n\n-- Evaluate message 0 with Text: Your basket is empty.\n-- These equations are tried from top to bottom.\n-- _ matches any remaining input: this is the fallback.\n-- Try message 1, then message 4, using Text.\n\n-- Exercise: add a message for 2 items before the fallback equation.\n-- Keep the equations together so they define one function.\n-- Next: try a Practical Example, or browse Language reference.\n", - "sourceSha256": "85ec05530278f77521c28f25437ff85b9418656eb7174a5ca7aba3abb826167f", - "export": "message", - "arguments": [ - { - "type": "number", - "value": "0" - } - ], + "hash": "dd0931401769303bf2e1ce08c51e7778779ccc9643ca8315c256c0542eedff7a", + "source": "!export *\n\n-- Choose a message by matching an input.\nmessage 0 = \"Your basket is empty.\"\nmessage 1 = \"You have one item.\"\nmessage _ = \"You have several items.\"\n\n-- These equations are tried from top to bottom.\n-- _ matches any remaining input: this is the fallback.\nemptyBasket = message 0\noneItem = message 1\nfullBasket = message 4\n\n-- Try adding a message for 2 items before the fallback equation.\n-- Keep the equations together so they define one function.\n-- Next: a Practical Example, or Language reference.\n", + "sourceSha256": "b3e3cbfdf958715f14dca4bfd0b69e58a6e1b250ee080cbddfa29b0711e0961d", + "export": "emptyBasket", + "arguments": [], "output": "string", "mode": "pure", "presets": { + "emptyBasket": { + "arguments": [], + "output": "string", + "mode": "pure" + }, + "oneItem": { + "arguments": [], + "output": "string", + "mode": "pure" + }, + "fullBasket": { + "arguments": [], + "output": "string", + "mode": "pure" + }, "message": { "arguments": [ { @@ -188,8 +228,8 @@ "title": "Language reference", "category": "Language & Runtime Explorations", "hash": "86717d7f9d69fe730c6c6172c42c49895623aa070bf675e1b5f751adb30616c1", - "source": "!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 *\n!import 7f87549d587fa8c3be88af0ea988db3adcc3b35f1ffb48973553491b075614db as Math\n!export *\n\n-- The open base import supplies contract helpers and unqualified names.\n-- Math is a qualified import: its names start with Math.\n-- A syntax sampler: independent examples, rather than one application.\n-- Start with answer / Natural number: 42. Choose a definition for its preset.\n-- Full contracts: follow For AI assistants in the footer to Language features.\n-- Each comment below gives an expression and its interpretation.\n\n-- Names, lambdas, named parameters, application and grouping.\nidentity = (x : x)\nfirst a b = a\nanswer = identity (first 42 7)\n-- identity \"hello\" / Text -> hello; first 7 9 / Natural number -> 7.\n\n-- The kernel constructors: Leaf, Stem and Fork. Use tricu for arbitrary trees.\nleaf = t\nshapes = [t (t t) (t t t)]\n-- leaf / tricu -> t; shapes / tricu shows a list containing all three shapes.\nliterals = [\"hello\" 42 [1 2 3]]\n-- literals / tricu shows mixed values. Strings support Unicode; names are ASCII.\n-- Lists use spaces. Group application items: [(identity 1) (identity 2)].\n\n-- Partial application: provide one of a function's arguments now.\naddTwo = Math.add 2\n-- addTwo 5 / Natural number -> 7.\n\n-- Ordered equations: literals, a qualified pin, exact lists, cons and wildcard.\n-- A pin (^true) compares against an existing value.\ndescribe 7 = \"seven\"\ndescribe \"hi\" = \"greeting\"\ndescribe ^true = \"true\"\ndescribe [a b] = \"two items\"\ndescribe [head | tail] = \"nonempty list\"\ndescribe [] = \"empty list\"\ndescribe _ = \"other\"\n-- Try each with Text: describe 7, describe \"hi\", describe (t t),\n-- describe [4 5], describe [1 2 3], describe [], describe (t (t t)).\n\n-- Pair patterns and whole-value capture. Construct a pair with t left right.\nkeep whole{left, right} = whole\nkeep _ = t\n-- keep (t t t) / tricu -> (t t t); keep (t t) / tricu -> t.\n\n-- Plain local bindings are sequential and nonrecursive.\nlocal x = let\n double n = Math.multiply n 2\n in double x\nviaWhere x = step x where step y = Math.add y 1\n-- local 6 -> 12; viaWhere 6 -> 7, both Natural number.\n\n-- Direct self-recursion, with a final fallback equation.\ncopy [] = []\ncopy [head | tail] = t head <| copy tail\ncopy _ = []\n-- copy [1 2 3] / Bytes -> [1, 2, 3]. Bytes displays naturals from 0 through 255.\n-- Use tricu for a general list; copy [] and copy (t t) both return [].\n\n-- case examines an intermediate value; arms align and finish with a fallback.\nfirstOr fallback xs = case xs of\n [head | _] = head\n _ = fallback\n-- firstOr 99 [5] -> 5; firstOr 99 [] -> 99, both Natural number.\n\n-- Local self-recursion is explicit: let rec (also available with where rec).\nlocalCopy xs = let rec\n walk [] = []\n walk [head | tail] = t head <| walk tail\n walk _ = []\n in walk xs\n-- localCopy [4 5] / Bytes -> [4, 5]; localCopy [] -> [].\n\n-- Parameter and result contracts check values at runtime.\nchecked x@nat? =@nat? x\n-- checked 42 / Natural number -> 42. A failed contract produces a Result.\n-- Keep annotations on plain function heads; see Contracts for Result handlers.\n\n-- Pipes and do use an explicit sequencing operator. Here a lambda passes a value\n-- to the next continuation. It is ordinary pure code; IO uses IO.bind instead.\nsequenced input = do (value next : next value)\n value <- input |> identity\n identity value\n identity <| value\n-- sequenced 9 / Natural number -> 9.\n-- Both pipe chains associate left-to-right; group nested applications explicitly.\n\n|- Block comments are another way to annotate source. They do not nest. -|\n-- !export * selects local definitions. Use !export name for a smaller interface.\n-- An open import uses !import MANIFEST *; a reexport uses !export * from MANIFEST.\n-- Replace MANIFEST with the full identity from Add import or the library guide.\n-- Reexports select the public interface; imports introduce names into scope.\n-- See Conversation for IO, and the docs for Maybe/Result/parser protocols.\n", - "sourceSha256": "593b2982c50668b5e8892eb8447f18df71011cabd7f5227a43181886519f519b", + "source": "!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 *\n!import 7f87549d587fa8c3be88af0ea988db3adcc3b35f1ffb48973553491b075614db as Math\n!export *\n\n-- The open base import supplies contract helpers and unqualified names.\n-- Math is a qualified import: its names start with Math.\n-- A syntax sampler: independent examples, rather than one application.\n-- Start with answer: its value is 42.\n-- Full contracts: follow For AI assistants in the footer to Language features.\n-- Comments give sample calls and interpretations; name a call to try it.\n\n-- Names, lambdas, named parameters, application and grouping.\nidentity = (x : x)\nfirst a b = a\nanswer = identity (first 42 7)\n-- identity \"hello\" / Text -> hello; first 7 9 / Natural number -> 7.\n\n-- The kernel constructors: Leaf, Stem and Fork. Use tricu for arbitrary trees.\nleaf = t\nshapes = [t (t t) (t t t)]\n-- leaf / tricu -> t; shapes / tricu shows a list containing all three shapes.\nliterals = [\"hello\" 42 [1 2 3]]\n-- literals / tricu shows mixed values. Strings support Unicode; names are ASCII.\n-- Lists use spaces. Group application items: [(identity 1) (identity 2)].\n\n-- Partial application: provide one of a function's arguments now.\naddTwo = Math.add 2\n-- addTwo 5 / Natural number -> 7.\n\n-- Ordered equations: literals, a pin, exact lists, cons and wildcard.\n-- A pin (^true) compares against an existing value.\ndescribe 7 = \"seven\"\ndescribe \"hi\" = \"greeting\"\ndescribe ^true = \"true\"\ndescribe [a b] = \"two items\"\ndescribe [head | tail] = \"nonempty list\"\ndescribe [] = \"empty list\"\ndescribe _ = \"other\"\n-- Try each with Text: describe 7, describe \"hi\", describe (t t),\n-- describe [4 5], describe [1 2 3], describe [], describe (t (t t)).\n\n-- Pair patterns and whole-value capture. Construct a pair with t left right.\nkeep whole{left, right} = whole\nkeep _ = t\n-- keep (t t t) / tricu -> (t t t); keep (t t) / tricu -> t.\n\n-- Plain local bindings are sequential and nonrecursive.\nlocal x = let\n double n = Math.multiply n 2\n in double x\nviaWhere x = step x where step y = Math.add y 1\n-- local 6 -> 12; viaWhere 6 -> 7, both Natural number.\n\n-- Direct self-recursion, with a final fallback equation.\ncopy [] = []\ncopy [head | tail] = t head <| copy tail\ncopy _ = []\n-- copy [1 2 3] / Bytes -> [1, 2, 3]. Bytes displays naturals from 0 through 255.\n-- Use tricu for a general list; copy [] and copy (t t) both return [].\n\n-- case examines an intermediate value; arms align and finish with a fallback.\nfirstOr fallback xs = case xs of\n [head | _] = head\n _ = fallback\n-- firstOr 99 [5] -> 5; firstOr 99 [] -> 99, both Natural number.\n\n-- Local self-recursion is explicit: let rec (also available with where rec).\nlocalCopy xs = let rec\n walk [] = []\n walk [head | tail] = t head <| walk tail\n walk _ = []\n in walk xs\n-- localCopy [4 5] / Bytes -> [4, 5]; localCopy [] -> [].\n\n-- Parameter and result contracts check values at runtime.\nchecked x@nat? =@nat? x\n-- checked 42 / Natural number -> 42. A failed contract produces a Result.\n-- Keep annotations on plain function heads; see Contracts for Result handlers.\n\n-- Pipes and do use an explicit sequencing operator. Here a lambda passes a value\n-- to the next continuation. It is ordinary pure code; IO uses IO.bind instead.\nsequenced input = do (value next : next value)\n value <- input |> identity\n identity value\n identity <| value\n-- sequenced 9 / Natural number -> 9.\n-- Both pipe chains associate left-to-right; group nested applications explicitly.\n\n|- Block comments are another way to annotate source. They do not nest. -|\n-- !export * selects local definitions. Use !export name for a smaller interface.\n-- An open import uses !import MANIFEST *; a reexport uses !export * from MANIFEST.\n-- Replace MANIFEST with the full identity from Explore libraries or the library guide.\n-- Reexports select the public interface; imports introduce names into scope.\n-- See Conversation for IO, and the docs for Maybe/Result/parser protocols.\n", + "sourceSha256": "d464c7964012c904cc322e957990be26c4172e11eea745ef5f75d4cd03a6635f", "export": "answer", "arguments": [], "output": "number", @@ -378,8 +418,8 @@ "title": "A conversation", "category": "Practical Examples", "hash": "b6f32449d85257fbd0a589d29ef05ea4a503c4a7e6105926d408b0551a34fd77", - "source": "!import 3dbb76ee4b350c161ed4f9c9f600034195b2fcf99202d46b8adcd25715a56f6b as IO\n!import 11791107bece9dac78ff92dcec75d11830921e359976f561a934b36f9e8910df as Text\n!export main\n\n-- Run main, then Send a line or choose End input in the terminal.\n-- Read and write Results are checked before continuing.\n-- Inspect the returned name as Text. Try changing the greeting.\ngreet writer [[\"line\" name _] _] = do IO.require\n Text.writeText writer \"Hello, \"\n Text.writeText writer name\n Text.writeText writer \"!\\n\"\n IO.pure name\ngreet writer [[\"eof\"] _] = IO.require\n (Text.writeText writer \"End of input.\\n\") (_ : IO.pure \"\")\ngreet _ _ = IO.fail \"Invalid line reply\"\n\nmain = IO.io\n do IO.require\n reader <- IO.stdin\n writer <- IO.stdout\n Text.writeText writer \"What is your name?\\n\"\n line <- Text.readLine (Text.lineCursor reader) 4096\n greet writer line\n", - "sourceSha256": "9db644e07fa3c58e12b19f74ba0eedadb7e5ad056fe9432ee7599f09fbdc309b", + "source": "!import 3dbb76ee4b350c161ed4f9c9f600034195b2fcf99202d46b8adcd25715a56f6b as IO\n!import 11791107bece9dac78ff92dcec75d11830921e359976f561a934b36f9e8910df as Text\n!export main\n\n-- A conversation: ask for a name, then greet the person who replies.\n-- Send a line or choose \"End input\" in the card's terminal.\n-- Read and write Results are checked before continuing.\n-- Return the name when IO finishes. Try changing the greeting.\ngreet writer [[\"line\" name _] _] = do IO.require\n Text.writeText writer \"Hello, \"\n Text.writeText writer name\n Text.writeText writer \"!\\n\"\n IO.pure name\ngreet writer [[\"eof\"] _] = IO.require\n (Text.writeText writer \"End of input.\\n\") (_ : IO.pure \"\")\ngreet _ _ = IO.fail \"Invalid line reply\"\n\nmain = IO.io\n do IO.require\n reader <- IO.stdin\n writer <- IO.stdout\n Text.writeText writer \"What is your name?\\n\"\n line <- Text.readLine (Text.lineCursor reader) 4096\n greet writer line\n", + "sourceSha256": "269f5ee9dcfa6cada4d18597fb35b2a1354535f204d38e3aab7d03621ad7adad", "export": "main", "arguments": [], "output": "string", @@ -392,8 +432,8 @@ "title": "Tree microscope", "category": "Language & Runtime Explorations", "hash": "493a125b649639c0df8348b4aeb3a455b7139cae0634ba114866c48813500bc5", - "source": "!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 as B\n!export shape\n!export size\n!export same\n\n-- Inspect data OR compiled program trees, not source text.\n-- Try shape (t t t), size (t t t), or same (t t) (t t).\n-- Also try shape (x : x): functions are inspectable trees too.\n-- triage dispatches directly on Leaf, Stem and Fork without visiting children.\n-- Arithmetic and Booleans come from base; traversal is the subject of this demo.\nshape tree = B.triage \"Leaf\" (child : \"Stem\") (left right : \"Fork\") tree\n\n-- Count every constructor occurrence, including repeated shared branches.\n-- This is expanded tree size, not allocated nodes or source-code length.\ncount tree acc = B.triage\n (B.succ acc)\n (child : count child <| B.succ acc)\n (left right : B.succ acc |> count left |> count right)\n tree\n\nsize tree = count tree 0\n\nsame a b = B.triage\n (B.triage B.true (ignored : B.false) (ignored ignored2 : B.false) b)\n (x : B.triage B.false (y : same x y) (ignored ignored2 : B.false) b)\n (x y : B.triage B.false (ignored : B.false) (u v : B.and? (same x u) (same y v)) b)\n a\n", - "sourceSha256": "08c2b341d8947c9e46062dfa63f9d8810eb15620de499caf0891e313c5152cef", + "source": "!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 *\n!export shape\n!export size\n!export same\n\n-- Inspect data OR compiled program trees, not source text.\n-- Try shape (t t t), size (t t t), or same (t t) (t t).\n-- Also try shape (x : x): functions are inspectable trees too.\n-- triage dispatches directly on Leaf, Stem and Fork without visiting children.\n-- Arithmetic and Booleans come from base; traversal is the subject of this demo.\nshape tree = triage \"Leaf\" (child : \"Stem\") (left right : \"Fork\") tree\n\n-- Count every constructor occurrence, including repeated shared branches.\n-- This is expanded tree size, not allocated nodes or source-code length.\ncountNodes tree acc = triage\n (succ acc)\n (child : countNodes child <| succ acc)\n (left right : succ acc |> countNodes left |> countNodes right)\n tree\n\nsize tree = countNodes tree 0\n\nsame a b = triage\n (triage true (ignored : false) (ignored ignored2 : false) b)\n (x : triage false (y : same x y) (ignored ignored2 : false) b)\n (x y : triage false (ignored : false) (u v : and? (same x u) (same y v)) b)\n a\n", + "sourceSha256": "ac4bd61b5953c523e29a42387acfc6305075066edf6c709283b02de19c51025c", "export": "size", "arguments": [ { @@ -446,8 +486,8 @@ "title": "RGB validator", "category": "Practical Examples", "hash": "b8c0e21c09f63511794cc62827a6b231e62a55f7e779022a1b740aaba29b0368", - "source": "!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 *\n!import 7a24af56a9ce9b947cadcafd3a9da127b1507275bfac62b9fa55e27490b176dc as Binary\n!export main\n\n-- Validate an RGB list using the library's canonical-byte predicate.\n-- Try main [64 128 255], main [0 256 255], or main [0 1].\n-- Canonical encoding matters: redundant high zero bits are rejected too.\n-- List-shape errors are separate from invalid channels; checks short-circuit.\nmain (t _) = \"Expected a list.\"\nmain [_ | (t _)] = \"Expected a list.\"\nmain [_ | [_ | (t _)]] = \"Expected a list.\"\nmain [r g b] = case and? (Binary.binaryByte? r) (and? (Binary.binaryByte? g) (Binary.binaryByte? b)) of\n ^true = \"RGB triple accepted.\"\n _ = \"A channel is not a canonical byte (0-255).\"\nmain _ = \"Expected exactly three channels.\"\n", - "sourceSha256": "33da7deb793df280f59b47913b57562dbb2820525f19451ba5e4042219ef559e", + "source": "!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 *\n!import 7a24af56a9ce9b947cadcafd3a9da127b1507275bfac62b9fa55e27490b176dc *\n!export main\n\n-- Validate an RGB list using the library's canonical-byte predicate.\n-- Try result = main [64 128 255].\n-- Try changing its input to [0 256 255], then [0 1].\n-- Canonical encoding matters: redundant high zero bits are rejected too.\n-- List-shape errors are separate from invalid channels; checks short-circuit.\nmain (t _) = \"Expected a list.\"\nmain [_ | (t _)] = \"Expected a list.\"\nmain [_ | [_ | (t _)]] = \"Expected a list.\"\nmain [r g b] = case and? (binaryByte? r) (and? (binaryByte? g) (binaryByte? b)) of\n ^true = \"RGB triple accepted.\"\n _ = \"A channel is not a canonical byte (0-255).\"\nmain _ = \"Expected exactly three channels.\"\n", + "sourceSha256": "721a08c37746e7b982994a7981b8fcff7312d889dac3276e7e72ca8cc06ed87a", "export": "main", "arguments": [ { @@ -469,8 +509,8 @@ "title": "Shipping policies", "category": "Practical Examples", "hash": "4aa9ce9f19dfce87ecb3f6bd0a2e881dafcbcd3d2e4b4075befbb378d80833ea", - "source": "!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 *\n!export v1\n!export v2\n\n-- Shipping policy: arguments are destination, cart subtotal in cents, grams.\n-- Destinations: domestic, regional, international. Grams: 1..5000.\n-- Select v1/v2 with the same cart: domestic free shipping changes from $75 to $100.\n-- Otherwise domestic costs $4.99/$5.99, regional $9.99, international $19.99.\n-- Packages above 1kg add $2.50. Subtotals are bounded to $10,000.\n-- The bundle hash + export pins the policy used to reproduce a quote later.\n-- Records use [fieldName tag value]: 0 = text, 1 = natural, 2 = Boolean.\n-- Base supplies canonical-natural checks, arithmetic and Booleans.\n\nfailure message = [[\"status\" 0 \"error\"] [\"message\" 0 message]]\n\nbaseRate 1 \"domestic\" = 499\nbaseRate _ \"domestic\" = 599\nbaseRate _ \"regional\" = 999\nbaseRate _ _ = 1999\n\npaidRate version zone grams = case lte_? grams 1000 of\n ^true = baseRate version zone\n _ = add 250 <| baseRate version zone\n\nfreeThreshold 1 = 7500\nfreeThreshold _ = 10000\n\namount version zone subtotal grams = case and? (equal? zone \"domestic\") (lte_? (freeThreshold version) subtotal) of\n ^true = 0\n _ = paidRate version zone grams\n\nquote version zone subtotal grams = case and? (isNat? subtotal) (isNat? grams) of\n ^true = case or? (equal? zone \"domestic\") (or? (equal? zone \"regional\") (equal? zone \"international\")) of\n ^true = case and? (lte_? subtotal 1000000) (and? (lte_? 1 grams) (lte_? grams 5000)) of\n ^true = [[\"status\" 0 \"ok\"] [\"policyVersion\" 1 version]\n [\"destination\" 0 zone] [\"shippingCents\" 1 (amount version zone subtotal grams)]]\n _ = failure \"Subtotal must be <=1000000 cents; grams must be 1..5000.\"\n _ = failure \"Unknown destination.\"\n _ = failure \"Subtotal and grams must be canonical natural numbers.\"\n\nv1 zone subtotal grams = quote 1 zone subtotal grams\nv2 zone subtotal grams = quote 2 zone subtotal grams\n", - "sourceSha256": "dbbb53a9da0918c294f8d3b07768a756cae7d6beafb3c780caba3e6d4c0f7d4a", + "source": "!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 *\n!export v1\n!export v2\n\n-- Shipping policy: arguments are destination, cart subtotal in cents, grams.\n-- Destinations: domestic, regional, international. Grams: 1..5000.\n-- Try result = v1 \"domestic\" 8000 1500.\n-- Change v1 to v2: domestic free shipping changes from $75 to $100.\n-- Otherwise domestic costs $4.99/$5.99, regional $9.99, international $19.99.\n-- Packages above 1kg add $2.50. Subtotals are bounded to $10,000.\n-- The bundle hash + export pins the policy used to reproduce a quote later.\n-- Records use [fieldName tag value]: 0 = text, 1 = natural, 2 = Boolean.\n-- Base supplies canonical-natural checks, arithmetic and Booleans.\n\nfailure message = [[\"status\" 0 \"error\"] [\"message\" 0 message]]\n\nbaseRate 1 \"domestic\" = 499\nbaseRate _ \"domestic\" = 599\nbaseRate _ \"regional\" = 999\nbaseRate _ _ = 1999\n\npaidRate version zone grams = case lte_? grams 1000 of\n ^true = baseRate version zone\n _ = add 250 <| baseRate version zone\n\nfreeThreshold 1 = 7500\nfreeThreshold _ = 10000\n\namount version zone subtotal grams = case and? (equal? zone \"domestic\") (lte_? (freeThreshold version) subtotal) of\n ^true = 0\n _ = paidRate version zone grams\n\nquote version zone subtotal grams = case and? (isNat? subtotal) (isNat? grams) of\n ^true = case or? (equal? zone \"domestic\") (or? (equal? zone \"regional\") (equal? zone \"international\")) of\n ^true = case and? (lte_? subtotal 1000000) (and? (lte_? 1 grams) (lte_? grams 5000)) of\n ^true = [[\"status\" 0 \"ok\"] [\"policyVersion\" 1 version]\n [\"destination\" 0 zone] [\"shippingCents\" 1 (amount version zone subtotal grams)]]\n _ = failure \"Subtotal must be <=1000000 cents; grams must be 1..5000.\"\n _ = failure \"Unknown destination.\"\n _ = failure \"Subtotal and grams must be canonical natural numbers.\"\n\nv1 zone subtotal grams = quote 1 zone subtotal grams\nv2 zone subtotal grams = quote 2 zone subtotal grams\n", + "sourceSha256": "790497b064d8093290af112d8c1cc072c93da25aea4f288e87d7abefa9750e7f", "export": "v1", "arguments": [ { @@ -533,8 +573,8 @@ "title": "Event adapters", "category": "Practical Examples", "hash": "8b51693c63658d59c8083ec1c95cf1f6565ce5799791f540e92caf508613c9fb", - "source": "!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 *\n!import 3f70d95cf1bae0928ee89acd2a9e0cdd8287796efae32a9f08d33e3dd0149c55 as A\n!export vendorA\n!export vendorB\n\n-- Event adapter: two producers, one consumer-facing record schema.\n-- vendorA accepts [eventName customer totalCents]. eventName is purchase/refund.\n-- vendorB accepts [customer refunded totalCents]. refunded is a Boolean.\n-- Both return schemaVersion, eventType, customer and amountCents.\n-- Try the two exports: their presets describe the same order.created event.\n-- A producer can publish the adapter hash alongside its event schema.\n-- Records use [fieldName tag value]: 0 = text, 1 = natural, 2 = Boolean.\n-- Base supplies arithmetic/Booleans; A.hostString? checks Unicode scalar text.\n\nfailure message = [[\"status\" 0 \"error\"] [\"message\" 0 message]]\n\nnormalized event customer cents = case A.hostString? customer of\n ^true = case and? (isNat? cents) (lte_? cents 1000000) of\n ^true = [[\"schemaVersion\" 1 1] [\"eventType\" 0 event]\n [\"customer\" 0 customer] [\"amountCents\" 1 cents]]\n _ = failure \"amountCents must be a canonical natural <=1000000.\"\n _ = failure \"customer must be scalar text.\"\n\nfromA \"purchase\" customer cents = normalized \"order.created\" customer cents\nfromA \"refund\" customer cents = normalized \"order.refunded\" customer cents\nfromA _ _ _ = failure \"Unknown vendor A event.\"\n\nfromB customer ^true cents = normalized \"order.refunded\" customer cents\nfromB customer ^false cents = normalized \"order.created\" customer cents\nfromB _ _ _ = failure \"refunded must be a canonical Boolean.\"\n\n-- Preserve list-shape diagnostics without inspecting any field before dispatch.\nunpack handler (t _) = failure \"Expected a list.\"\nunpack handler [_ | (t _)] = failure \"Expected a list.\"\nunpack handler [_ | [_ | (t _)]] = failure \"Expected a list.\"\nunpack handler [a b c] = handler a b c\nunpack _ _ = failure \"Expected exactly three fields.\"\n\nvendorA values = unpack fromA values\nvendorB values = unpack fromB values\n", - "sourceSha256": "08a7857e4cb6ad2e4c1c9972f3ce05f26b810af8481ca974b91b1819bd4718e7", + "source": "!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 *\n!import 3f70d95cf1bae0928ee89acd2a9e0cdd8287796efae32a9f08d33e3dd0149c55 *\n!export vendorA\n!export vendorB\n\n-- Event adapter: two producers, one consumer-facing record schema.\n-- vendorA accepts [eventName customer totalCents]. eventName is purchase/refund.\n-- vendorB accepts [customer refunded totalCents]. refunded is a Boolean.\n-- Both return schemaVersion, eventType, customer and amountCents.\n-- Try result = vendorA [\"purchase\" \"James\" 2499].\n-- Try result = vendorB [\"James\" false 2499] to describe the same event.\n-- A producer can publish the adapter hash alongside its event schema.\n-- Records use [fieldName tag value]: 0 = text, 1 = natural, 2 = Boolean.\n-- Base supplies arithmetic/Booleans; hostString? checks Unicode scalar text.\n\nfailure message = [[\"status\" 0 \"error\"] [\"message\" 0 message]]\n\nnormalized event customer cents = case hostString? customer of\n ^true = case and? (isNat? cents) (lte_? cents 1000000) of\n ^true = [[\"schemaVersion\" 1 1] [\"eventType\" 0 event]\n [\"customer\" 0 customer] [\"amountCents\" 1 cents]]\n _ = failure \"amountCents must be a canonical natural <=1000000.\"\n _ = failure \"customer must be scalar text.\"\n\nfromA \"purchase\" customer cents = normalized \"order.created\" customer cents\nfromA \"refund\" customer cents = normalized \"order.refunded\" customer cents\nfromA _ _ _ = failure \"Unknown vendor A event.\"\n\nfromB customer ^true cents = normalized \"order.refunded\" customer cents\nfromB customer ^false cents = normalized \"order.created\" customer cents\nfromB _ _ _ = failure \"refunded must be a canonical Boolean.\"\n\n-- Preserve list-shape diagnostics without inspecting any field before dispatch.\nunpack handler (t _) = failure \"Expected a list.\"\nunpack handler [_ | (t _)] = failure \"Expected a list.\"\nunpack handler [_ | [_ | (t _)]] = failure \"Expected a list.\"\nunpack handler [a b c] = handler a b c\nunpack _ _ = failure \"Expected exactly three fields.\"\n\nvendorA values = unpack fromA values\nvendorB values = unpack fromB values\n", + "sourceSha256": "3d086d7b0dba582e95e28515747c6d80cd4ab0af3c825af30fd759b1788527e3", "export": "vendorA", "arguments": [ { @@ -612,8 +652,8 @@ "title": "Registration validator", "category": "Practical Examples", "hash": "679733e17a86cc5e3f5282ca824cad4c9c0febd260e51f6a87e16dd14c02c45a", - "source": "!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 *\n!import 3f70d95cf1bae0928ee89acd2a9e0cdd8287796efae32a9f08d33e3dd0149c55 as A\n!export main\n\n-- Registration validator: name, email, age, termsAccepted are separate arguments.\n-- Returns one field-specific message per input, plus an accepted Boolean.\n-- Name must be nonempty scalar text. Age must be a natural from 18 to 120.\n-- Email uses a deliberately small rule: nonempty text containing @ and a dot.\n-- Terms must be the canonical true Boolean. This is a teaching policy, not SMTP validation.\n-- Browser and server can pin this same executable instead of duplicating rules.\n-- Records use [fieldName tag value]: 0 = text, 1 = natural, 2 = Boolean.\n-- Base supplies arithmetic/Booleans; A.hostString? checks Unicode scalar text.\n\nnameMessage name = case A.hostString? name of\n ^true = case name of\n [] = \"Name is required.\"\n _ = \"\"\n _ = \"Name must be scalar text.\"\n\nemailMessage email = case A.hostString? email of\n ^true = case and? (contains? \"@\" email) (contains? \".\" email) of\n ^true = \"\"\n _ = \"Email must contain @ and a dot.\"\n _ = \"Email must be scalar text.\"\n\nageMessage age = case isNat? age of\n ^true = case and? (lte_? 18 age) (lte_? age 120) of\n ^true = \"\"\n _ = \"Age must be from 18 to 120.\"\n _ = \"Age must be a canonical natural.\"\n\ntermsMessage ^true = \"\"\ntermsMessage _ = \"Accept the terms.\"\n\nreport name email age terms = [\n [\"accepted\" 2 (and? (equal? name \"\") (and? (equal? email \"\") (and? (equal? age \"\") (equal? terms \"\"))))]\n [\"nameError\" 0 name] [\"emailError\" 0 email]\n [\"ageError\" 0 age] [\"termsError\" 0 terms]]\n\nmain name email age terms = report (nameMessage name) (emailMessage email) (ageMessage age) (termsMessage terms)\n", - "sourceSha256": "b15d4e60a34d839b06c813fa439ea063abc4a771bc2d0dd24cd928eb21ca7e29", + "source": "!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 *\n!import 3f70d95cf1bae0928ee89acd2a9e0cdd8287796efae32a9f08d33e3dd0149c55 *\n!export main\n\n-- Registration validator: name, email, age, termsAccepted are separate arguments.\n-- Returns one field-specific message per input, plus an accepted Boolean.\n-- Try result = main \"James\" \"james@example.org\" 21 true.\n-- Try changing the age to 17 or the email to \"bad\" to see field messages.\n-- Name must be nonempty scalar text. Age must be a natural from 18 to 120.\n-- Email uses a deliberately small rule: nonempty text containing @ and a dot.\n-- Terms must be the canonical true Boolean. This is a teaching policy, not SMTP validation.\n-- Browser and server can pin this same executable instead of duplicating rules.\n-- Records use [fieldName tag value]: 0 = text, 1 = natural, 2 = Boolean.\n-- Base supplies arithmetic/Booleans; hostString? checks Unicode scalar text.\n\nnameMessage name = case hostString? name of\n ^true = case name of\n [] = \"Name is required.\"\n _ = \"\"\n _ = \"Name must be scalar text.\"\n\nemailMessage email = case hostString? email of\n ^true = case and? (contains? \"@\" email) (contains? \".\" email) of\n ^true = \"\"\n _ = \"Email must contain @ and a dot.\"\n _ = \"Email must be scalar text.\"\n\nageMessage age = case isNat? age of\n ^true = case and? (lte_? 18 age) (lte_? age 120) of\n ^true = \"\"\n _ = \"Age must be from 18 to 120.\"\n _ = \"Age must be a canonical natural.\"\n\ntermsMessage ^true = \"\"\ntermsMessage _ = \"Accept the terms.\"\n\nreport name email age terms = [\n [\"accepted\" 2 (and? (equal? name \"\") (and? (equal? email \"\") (and? (equal? age \"\") (equal? terms \"\"))))]\n [\"nameError\" 0 name] [\"emailError\" 0 email]\n [\"ageError\" 0 age] [\"termsError\" 0 terms]]\n\nmain name email age terms = report (nameMessage name) (emailMessage email) (ageMessage age) (termsMessage terms)\n", + "sourceSha256": "c91388231dfc9a04da02e750cfcbd873947f6cf761a59352446000421c7806dd", "export": "main", "arguments": [ { @@ -666,8 +706,8 @@ "title": "Profile migrations", "category": "Practical Examples", "hash": "730aeb8203ff504b7483999a6477a292b10871f82e94a77d3d5f7b49ecc3b6b3", - "source": "!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 *\n!import 3f70d95cf1bae0928ee89acd2a9e0cdd8287796efae32a9f08d33e3dd0149c55 as A\n!export v1\n!export v2\n\n-- Archived profile v0: [name email]. v1: adds schemaVersion and displayName.\n-- v2: also supplies marketingOptIn=false; existing data is preserved.\n-- Select v1/v2 with the same input to compare the two target schemas.\n-- A migration receipt can record the input identity, executable hash/export,\n-- output identity and wire schema. Running this demo stays entirely local.\n-- Records use [fieldName tag value]: 0 = text, 1 = natural, 2 = Boolean.\n-- Base supplies arithmetic/Booleans; A.hostString? checks Unicode scalar text.\n\nfailure message = [[\"status\" 0 \"error\"] [\"message\" 0 message]]\n\nmigrate version name email = case and? (A.hostString? name) (A.hostString? email) of\n ^true = case version of\n 1 = [[\"schemaVersion\" 1 1] [\"displayName\" 0 name] [\"email\" 0 email]]\n _ = [[\"schemaVersion\" 1 2] [\"displayName\" 0 name] [\"email\" 0 email]\n [\"marketingOptIn\" 2 false]]\n _ = failure \"Name and email must be scalar text.\"\n\nprofile _ (t _) = failure \"Expected a list.\"\nprofile _ [_ | (t _)] = failure \"Expected a list.\"\nprofile version [name email] = migrate version name email\nprofile _ _ = failure \"Expected [name email].\"\n\nv1 values = profile 1 values\nv2 values = profile 2 values\n", - "sourceSha256": "a9270baef5cb5102c895c9f23434ebc82c87eebfa16c25cb415b06b4636a279e", + "source": "!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 *\n!import 3f70d95cf1bae0928ee89acd2a9e0cdd8287796efae32a9f08d33e3dd0149c55 *\n!export v1\n!export v2\n\n-- Archived profile v0: [name email]. v1: adds schemaVersion and displayName.\n-- v2: also supplies marketingOptIn=false; existing data is preserved.\n-- Try result = v1 [\"James\" \"james@example.org\"].\n-- Change v1 to v2 to compare the two target schemas.\n-- A migration receipt can record the input identity, executable hash/export,\n-- output identity and wire schema. Running this demo stays entirely local.\n-- Records use [fieldName tag value]: 0 = text, 1 = natural, 2 = Boolean.\n-- Base supplies arithmetic/Booleans; hostString? checks Unicode scalar text.\n\nfailure message = [[\"status\" 0 \"error\"] [\"message\" 0 message]]\n\nmigrate version name email = case and? (hostString? name) (hostString? email) of\n ^true = case version of\n 1 = [[\"schemaVersion\" 1 1] [\"displayName\" 0 name] [\"email\" 0 email]]\n _ = [[\"schemaVersion\" 1 2] [\"displayName\" 0 name] [\"email\" 0 email]\n [\"marketingOptIn\" 2 false]]\n _ = failure \"Name and email must be scalar text.\"\n\nprofile _ (t _) = failure \"Expected a list.\"\nprofile _ [_ | (t _)] = failure \"Expected a list.\"\nprofile version [name email] = migrate version name email\nprofile _ _ = failure \"Expected [name email].\"\n\nv1 values = profile 1 values\nv2 values = profile 2 values\n", + "sourceSha256": "239e45bde8f461360a016bfbcca3111dc72da3b7dc237736d05b12dc6018e9f8", "export": "v1", "arguments": [ { diff --git a/ext/demo-site/demos.json b/ext/demo-site/demos.json @@ -5,13 +5,13 @@ "compilerExport": "source_unit.compileStableModuleDefinitions", "compilerBundleSha256": "faff942bf4539f70d3fd5b085d6435199307d71e07da18f4b144c0b0597eb897", "compilerManifest": "3dec0a4afa2327872dbd63247e86e40fbf85030188b5be799e1b82cb9f57f5b2", - "hostSha256": "0379ea68db3664a6672e7ae56338953935de3d15318d49a07fa634bded59af80", + "hostSha256": "3360e32a04be2efef002559962b2e1cb8f4ccca9d88e02d930959ac4ece75d1b", "demos": { "names-values": { "demandCases": [], - "source": "!export greeting\n!export answer\n\n-- Give a value a name.\ngreeting = \"Hello, world!\"\n-- Evaluate greeting with Text.\n-- Result: Hello, world!\n-- Try changing the words, then Evaluate again.\n\n-- Numbers can have names too.\nanswer = 42\n-- Choose answer in Definition.\n-- It selects Natural number; the result is 42.\n\n-- Exercise: change answer to your favourite number and evaluate it.\n-- The !export lines make our names available to the playground.\n-- Edits compile after a pause; Evaluate runs the expression.\n-- Next: Doing arithmetic.\n", - "sourceSha256": "77d8c61538436b86f620612a8502ba2980838b6e370cf71f6a364ff851517f3e", - "hash": "8d1b7e1f6dfefe983167ee8d9631476caaa0c3dc689a85628e9ca627ebaef6d0", + "source": "!export *\n\n-- Welcome to tricu! Anything after -- on a line is a comment.\n-- tricu is a lazy functional language based on tree calculus.\n\n-- Click or tap underlined definition names to inspect their values.\n-- Function calls can have names too; we'll try that in \"Your first function\".\n-- Edits compile after a pause. IO programs offer \"Run IO\" inside their card.\n\n-- tricu offers string literals,\ngreeting = \"Hello, world!\"\n\n-- natural number literals (whole numbers starting at zero),\nanswer = 42\n\n-- list literals,\ngreetAndAnswer = [greeting answer]\n\n-- and more! Choose \"Doing arithmetic\" from the \"Program\" dropdown next.\n", + "sourceSha256": "f91426877f7cbd74aa4de3f8241844e38bd724ed1c3766ee50e04f548a6a31fd", + "hash": "4116a3702923898d5fafc8c855fbea38a7fd740bdb15667ef53fec7268b41381", "mode": "pure", "haskellCases": [ { @@ -27,14 +27,21 @@ "input": null, "terminal": "", "result": "2021020210202100" + }, + { + "export": "greetAndAnswer", + "arguments": [], + "input": null, + "terminal": "", + "result": "2220202021020202100221020210202021021002202021021020210210022020210210202102100221021021021020210210022020210210202100220202020202100221021021020210210210022102102102102021021002202102020210210210022020210210202102100220202102020210210022102020202021000220210202102021000" } ] }, "arithmetic": { "demandCases": [], - "source": "!import 7f87549d587fa8c3be88af0ea988db3adcc3b35f1ffb48973553491b075614db as Math\n!export *\n\n-- Add the prices of two items.\ntotal = Math.add 5 7\n-- Evaluate total with Natural number.\n-- Result: 12.\n-- Add import supplies the library identity; Math is our short name.\n\n-- Function calls use spaces: Math.multiply takes two numbers.\nproduct = Math.multiply 5 7\n\n-- Parentheses make one calculation the input to another.\ngrouped = Math.multiply (Math.add 5 7) 2\n\n-- Try product (35), then grouped (24), with Natural number.\n-- Exercise: change total to add the prices of two things you would buy.\n-- Numbers here are whole numbers starting at zero.\n-- Next: Your first function.\n", - "sourceSha256": "67bfda56d700499e9f484f73d670c22156842fc963798127284be1f8482e2ba8", - "hash": "38110ab45a75a9c8771d6422277cb3083e667a4368aa33df0b43af672f17375e", + "source": "!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 *\n!export *\n\n-- Let's do some arithmetic! Our import supplies add and mul (multiply).\n-- Function calls use spaces between the function and its inputs.\ntotal = add 5 7\nmultiplied = mul 5 7\n\n-- Parentheses make one calculation the input to another.\ngrouped = mul (add 5 7) 2\n\n-- Try changing the numbers to calculate the cost of two things you'd buy.\n-- Next: Your first function.\n", + "sourceSha256": "de1aea17cb2cab0a386f491f1e08fe00aec48c80967740ee55756987c69d328d", + "hash": "ac504497f656b84b7ae1e7f9a00086c76d05686df3eee2c5612a8c7f51fbf141", "mode": "pure", "haskellCases": [ { @@ -45,7 +52,7 @@ "result": "20202102100" }, { - "export": "product", + "export": "multiplied", "arguments": [], "input": null, "terminal": "", @@ -62,12 +69,26 @@ }, "functions": { "demandCases": [], - "source": "!import 7f87549d587fa8c3be88af0ea988db3adcc3b35f1ffb48973553491b075614db as Math\n!export *\n\n-- Reuse a calculation with different inputs.\ndouble number = Math.multiply number 2\n-- Evaluate double 6 with Natural number.\n-- Result: 12.\n-- Add import supplies Math, including multiplication.\n\n-- number is a parameter: each call supplies its value.\n-- Try double 9: the same definition now returns 18.\n\n-- A function can also take more than one input.\nadd left right = Math.add left right\n-- Try add 4 7 with Natural number: the result is 11.\n\n-- Exercise: change double to multiply by 3, and give it the name triple.\n-- Then evaluate triple 6. Update the expression to use your new name.\n-- Next: Working with lists.\n", - "sourceSha256": "44560b2f0d1df0ef24b853cc0e9a31a9a57108d8cfa56653a1840d264e498a24", - "hash": "be5f4675c9b0b6d9c093fe44a9695da9f428c88d62b0f4f1340017067fa81fa9", + "source": "!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 *\n!export *\n\n-- Give a calculation a name so we can reuse it with different inputs.\ndouble number = mul number 2\n\n-- number is a parameter: each call supplies its value.\nsixDoubled = double 6\nnineDoubled = double 9\n\n-- A function can take more than one input.\nrectangle width height = mul width height\narea = rectangle 4 7\n\n-- Try writing triple and using it in place of double.\n-- Next: Working with lists.\n", + "sourceSha256": "3e07457439457ad90533a9e1ecee410d3648d001b7308c6e8aaf41f03644e2f7", + "hash": "6c85d85a51c0c3ed8fb9adc1a5c81c18313174b3f72371933e432f3f895c1667", "mode": "pure", "haskellCases": [ { + "export": "sixDoubled", + "arguments": [], + "input": null, + "terminal": "", + "result": "20202102100" + }, + { + "export": "nineDoubled", + "arguments": [], + "input": null, + "terminal": "", + "result": "2021020202100" + }, + { "export": "double", "arguments": [ "202102100" @@ -86,26 +107,33 @@ "result": "2021020202100" }, { - "export": "add", + "export": "rectangle", "arguments": [ "20202100", "2102102100" ], "input": null, "terminal": "", - "result": "210210202100" + "result": "20202102102100" + }, + { + "export": "area", + "arguments": [], + "input": null, + "terminal": "", + "result": "20202102102100" } ] }, "lists": { "demandCases": [], - "source": "!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 as List\n!export count\n!export total\n\n-- Collect values in a list.\nnumbers = [2 4 6]\ncount = List.length numbers\ntotal = List.sum numbers\n\n-- Evaluate count with Natural number: the result is 3.\n-- Spaces separate items. [] is the empty list.\n-- Add import supplies List, including length and sum.\n\n-- Try total with Natural number: the result is 12.\n-- Exercise: add 8 to numbers. Check count (4) and total (20).\n-- Then try numbers = []: both results become 0.\n-- Next: Transforming a list.\n", - "sourceSha256": "8d1dcc3840ccbfff3e2b64de18ba96ea81346b317dded4b7d3414657acf4fa7f", - "hash": "3cc5d36c5b70a20362117e3349aa044efca8adca56105424965065395a9f3e76", + "source": "!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 *\n!export *\n\n-- Collect values in a list. Spaces separate items; [] is the empty list.\nnumbers = [2 4 6]\nitemCount = length numbers\ntotal = sum numbers\n\n-- Try adding another item to numbers, then try numbers = [].\n-- Next: Transforming a list.\n", + "sourceSha256": "ba33f8097d479eeb3adcbcc92c7b33931970df2b7ea4d523535673a095e5a1b2", + "hash": "8eb72ff6766fb762614c6dca9026ebda78d06f5e8f78e48cc29b195ba2154b21", "mode": "pure", "haskellCases": [ { - "export": "count", + "export": "itemCount", "arguments": [], "input": null, "terminal": "", @@ -117,14 +145,21 @@ "input": null, "terminal": "", "result": "20202102100" + }, + { + "export": "numbers", + "arguments": [], + "input": null, + "terminal": "", + "result": "220210022020210022021021000" } ] }, "transform-list": { "demandCases": [], - "source": "!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 as List\n!import 7f87549d587fa8c3be88af0ea988db3adcc3b35f1ffb48973553491b075614db as Math\n!export total\n!export double\n\n-- Apply the same function to every item.\ndouble number = Math.multiply number 2\nnumbers = [2 4 6]\n\n-- map takes a function, then a list. Here it produces [4 8 12].\ndoubled = List.map double numbers\ntotal = List.sum doubled\n-- Evaluate total with Natural number: the result is 24.\n-- Add import supplies List and Math.\n\n-- Try double 5 with Natural number: the result is 10.\n-- Exercise: make double multiply by 3. The new total should be 36.\n-- Next: Making a choice.\n", - "sourceSha256": "8e33d918846404760b398b473a93effad390b1486d5c2b053078170226cb8da9", - "hash": "9f10d772b42628acfe843eec7524d8de1ba9ea4344d3952c6c091c98c4d1c72d", + "source": "!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 *\n!export *\n\n-- Functions are values too. We can pass one to another function.\ndouble number = mul number 2\nnumbers = [2 4 6]\n\n-- map takes a function, then a list, and applies it to every item.\ndoubled = map double numbers\n-- map builds a new list; the original numbers are unchanged.\ntotal = sum doubled\n\n-- Try multiplying by 3 instead.\n-- Next: Making a choice.\n", + "sourceSha256": "e752330fcedfc61065c695ebbafa1e6bc65745d890ee98c7682d4e5fe11bc45f", + "hash": "639c8d3959320ef99d9416257905f189519a90ea4b315cdbb0485a386e46191c", "mode": "pure", "haskellCases": [ { @@ -142,17 +177,52 @@ "input": null, "terminal": "", "result": "20210202100" + }, + { + "export": "numbers", + "arguments": [], + "input": null, + "terminal": "", + "result": "220210022020210022021021000" + }, + { + "export": "doubled", + "arguments": [], + "input": null, + "terminal": "", + "result": "220202100220202021002202021021000" } ] }, "choices": { "demandCases": [], - "source": "!export message\n\n-- Choose an answer by matching an input.\nmessage 0 = \"Your basket is empty.\"\nmessage 1 = \"You have one item.\"\nmessage _ = \"You have several items.\"\n\n-- Evaluate message 0 with Text: Your basket is empty.\n-- These equations are tried from top to bottom.\n-- _ matches any remaining input: this is the fallback.\n-- Try message 1, then message 4, using Text.\n\n-- Exercise: add a message for 2 items before the fallback equation.\n-- Keep the equations together so they define one function.\n-- Next: try a Practical Example, or browse Language reference.\n", - "sourceSha256": "85ec05530278f77521c28f25437ff85b9418656eb7174a5ca7aba3abb826167f", - "hash": "1972c98bfe599fef2a72cc946401a5cc9280026a8c46fdd7c8ccdc7093ee90cc", + "source": "!export *\n\n-- Choose a message by matching an input.\nmessage 0 = \"Your basket is empty.\"\nmessage 1 = \"You have one item.\"\nmessage _ = \"You have several items.\"\n\n-- These equations are tried from top to bottom.\n-- _ matches any remaining input: this is the fallback.\nemptyBasket = message 0\noneItem = message 1\nfullBasket = message 4\n\n-- Try adding a message for 2 items before the fallback equation.\n-- Keep the equations together so they define one function.\n-- Next: a Practical Example, or Language reference.\n", + "sourceSha256": "b3e3cbfdf958715f14dca4bfd0b69e58a6e1b250ee080cbddfa29b0711e0961d", + "hash": "dd0931401769303bf2e1ce08c51e7778779ccc9643ca8315c256c0542eedff7a", "mode": "pure", "haskellCases": [ { + "export": "emptyBasket", + "arguments": [], + "input": null, + "terminal": "", + "result": "2210202021021020210022102102102102021021002210202102021021021002202102020210210210022020202020210022021020202021021002210202020202102100221021020202102102100221021020210202102100221020210202021021002202021020210210210022020202020210022102020210202102100221021020202102102100220202020202100221020210202021021002210202102102021021002202020202102102100220202102021021021002210202021021021021002202102102102021000" + }, + { + "export": "oneItem", + "arguments": [], + "input": null, + "terminal": "", + "result": "22102020210210202100221021021021020210210022102021020210210210022020202020210022020202102021021002210202020202102100220210210202102102100221020210202021021002202020202021002210210210210202102100220210210210202102100221020210202021021002202020202021002210202021020210210022020210202102102100221020210202021021002210202102102021021002202102102102021000" + }, + { + "export": "fullBasket", + "arguments": [], + "input": null, + "terminal": "", + "result": "22102020210210202100221021021021020210210022102021020210210210022020202020210022020202102021021002210202020202102100220210210202102102100221020210202021021002202020202021002210210202021021021002210202102020210210022021021020210210210022102021020202102100220210202021021021002210202020202102100220202102102021021002202020202021002210202021020210210022020210202102102100221020210202021021002210202102102021021002210210202021021021002202102102102021000" + }, + { "export": "message", "arguments": [ "0" @@ -183,8 +253,8 @@ }, "language-reference": { "demandCases": [], - "source": "!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 *\n!import 7f87549d587fa8c3be88af0ea988db3adcc3b35f1ffb48973553491b075614db as Math\n!export *\n\n-- The open base import supplies contract helpers and unqualified names.\n-- Math is a qualified import: its names start with Math.\n-- A syntax sampler: independent examples, rather than one application.\n-- Start with answer / Natural number: 42. Choose a definition for its preset.\n-- Full contracts: follow For AI assistants in the footer to Language features.\n-- Each comment below gives an expression and its interpretation.\n\n-- Names, lambdas, named parameters, application and grouping.\nidentity = (x : x)\nfirst a b = a\nanswer = identity (first 42 7)\n-- identity \"hello\" / Text -> hello; first 7 9 / Natural number -> 7.\n\n-- The kernel constructors: Leaf, Stem and Fork. Use tricu for arbitrary trees.\nleaf = t\nshapes = [t (t t) (t t t)]\n-- leaf / tricu -> t; shapes / tricu shows a list containing all three shapes.\nliterals = [\"hello\" 42 [1 2 3]]\n-- literals / tricu shows mixed values. Strings support Unicode; names are ASCII.\n-- Lists use spaces. Group application items: [(identity 1) (identity 2)].\n\n-- Partial application: provide one of a function's arguments now.\naddTwo = Math.add 2\n-- addTwo 5 / Natural number -> 7.\n\n-- Ordered equations: literals, a qualified pin, exact lists, cons and wildcard.\n-- A pin (^true) compares against an existing value.\ndescribe 7 = \"seven\"\ndescribe \"hi\" = \"greeting\"\ndescribe ^true = \"true\"\ndescribe [a b] = \"two items\"\ndescribe [head | tail] = \"nonempty list\"\ndescribe [] = \"empty list\"\ndescribe _ = \"other\"\n-- Try each with Text: describe 7, describe \"hi\", describe (t t),\n-- describe [4 5], describe [1 2 3], describe [], describe (t (t t)).\n\n-- Pair patterns and whole-value capture. Construct a pair with t left right.\nkeep whole{left, right} = whole\nkeep _ = t\n-- keep (t t t) / tricu -> (t t t); keep (t t) / tricu -> t.\n\n-- Plain local bindings are sequential and nonrecursive.\nlocal x = let\n double n = Math.multiply n 2\n in double x\nviaWhere x = step x where step y = Math.add y 1\n-- local 6 -> 12; viaWhere 6 -> 7, both Natural number.\n\n-- Direct self-recursion, with a final fallback equation.\ncopy [] = []\ncopy [head | tail] = t head <| copy tail\ncopy _ = []\n-- copy [1 2 3] / Bytes -> [1, 2, 3]. Bytes displays naturals from 0 through 255.\n-- Use tricu for a general list; copy [] and copy (t t) both return [].\n\n-- case examines an intermediate value; arms align and finish with a fallback.\nfirstOr fallback xs = case xs of\n [head | _] = head\n _ = fallback\n-- firstOr 99 [5] -> 5; firstOr 99 [] -> 99, both Natural number.\n\n-- Local self-recursion is explicit: let rec (also available with where rec).\nlocalCopy xs = let rec\n walk [] = []\n walk [head | tail] = t head <| walk tail\n walk _ = []\n in walk xs\n-- localCopy [4 5] / Bytes -> [4, 5]; localCopy [] -> [].\n\n-- Parameter and result contracts check values at runtime.\nchecked x@nat? =@nat? x\n-- checked 42 / Natural number -> 42. A failed contract produces a Result.\n-- Keep annotations on plain function heads; see Contracts for Result handlers.\n\n-- Pipes and do use an explicit sequencing operator. Here a lambda passes a value\n-- to the next continuation. It is ordinary pure code; IO uses IO.bind instead.\nsequenced input = do (value next : next value)\n value <- input |> identity\n identity value\n identity <| value\n-- sequenced 9 / Natural number -> 9.\n-- Both pipe chains associate left-to-right; group nested applications explicitly.\n\n|- Block comments are another way to annotate source. They do not nest. -|\n-- !export * selects local definitions. Use !export name for a smaller interface.\n-- An open import uses !import MANIFEST *; a reexport uses !export * from MANIFEST.\n-- Replace MANIFEST with the full identity from Add import or the library guide.\n-- Reexports select the public interface; imports introduce names into scope.\n-- See Conversation for IO, and the docs for Maybe/Result/parser protocols.\n", - "sourceSha256": "593b2982c50668b5e8892eb8447f18df71011cabd7f5227a43181886519f519b", + "source": "!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 *\n!import 7f87549d587fa8c3be88af0ea988db3adcc3b35f1ffb48973553491b075614db as Math\n!export *\n\n-- The open base import supplies contract helpers and unqualified names.\n-- Math is a qualified import: its names start with Math.\n-- A syntax sampler: independent examples, rather than one application.\n-- Start with answer: its value is 42.\n-- Full contracts: follow For AI assistants in the footer to Language features.\n-- Comments give sample calls and interpretations; name a call to try it.\n\n-- Names, lambdas, named parameters, application and grouping.\nidentity = (x : x)\nfirst a b = a\nanswer = identity (first 42 7)\n-- identity \"hello\" / Text -> hello; first 7 9 / Natural number -> 7.\n\n-- The kernel constructors: Leaf, Stem and Fork. Use tricu for arbitrary trees.\nleaf = t\nshapes = [t (t t) (t t t)]\n-- leaf / tricu -> t; shapes / tricu shows a list containing all three shapes.\nliterals = [\"hello\" 42 [1 2 3]]\n-- literals / tricu shows mixed values. Strings support Unicode; names are ASCII.\n-- Lists use spaces. Group application items: [(identity 1) (identity 2)].\n\n-- Partial application: provide one of a function's arguments now.\naddTwo = Math.add 2\n-- addTwo 5 / Natural number -> 7.\n\n-- Ordered equations: literals, a pin, exact lists, cons and wildcard.\n-- A pin (^true) compares against an existing value.\ndescribe 7 = \"seven\"\ndescribe \"hi\" = \"greeting\"\ndescribe ^true = \"true\"\ndescribe [a b] = \"two items\"\ndescribe [head | tail] = \"nonempty list\"\ndescribe [] = \"empty list\"\ndescribe _ = \"other\"\n-- Try each with Text: describe 7, describe \"hi\", describe (t t),\n-- describe [4 5], describe [1 2 3], describe [], describe (t (t t)).\n\n-- Pair patterns and whole-value capture. Construct a pair with t left right.\nkeep whole{left, right} = whole\nkeep _ = t\n-- keep (t t t) / tricu -> (t t t); keep (t t) / tricu -> t.\n\n-- Plain local bindings are sequential and nonrecursive.\nlocal x = let\n double n = Math.multiply n 2\n in double x\nviaWhere x = step x where step y = Math.add y 1\n-- local 6 -> 12; viaWhere 6 -> 7, both Natural number.\n\n-- Direct self-recursion, with a final fallback equation.\ncopy [] = []\ncopy [head | tail] = t head <| copy tail\ncopy _ = []\n-- copy [1 2 3] / Bytes -> [1, 2, 3]. Bytes displays naturals from 0 through 255.\n-- Use tricu for a general list; copy [] and copy (t t) both return [].\n\n-- case examines an intermediate value; arms align and finish with a fallback.\nfirstOr fallback xs = case xs of\n [head | _] = head\n _ = fallback\n-- firstOr 99 [5] -> 5; firstOr 99 [] -> 99, both Natural number.\n\n-- Local self-recursion is explicit: let rec (also available with where rec).\nlocalCopy xs = let rec\n walk [] = []\n walk [head | tail] = t head <| walk tail\n walk _ = []\n in walk xs\n-- localCopy [4 5] / Bytes -> [4, 5]; localCopy [] -> [].\n\n-- Parameter and result contracts check values at runtime.\nchecked x@nat? =@nat? x\n-- checked 42 / Natural number -> 42. A failed contract produces a Result.\n-- Keep annotations on plain function heads; see Contracts for Result handlers.\n\n-- Pipes and do use an explicit sequencing operator. Here a lambda passes a value\n-- to the next continuation. It is ordinary pure code; IO uses IO.bind instead.\nsequenced input = do (value next : next value)\n value <- input |> identity\n identity value\n identity <| value\n-- sequenced 9 / Natural number -> 9.\n-- Both pipe chains associate left-to-right; group nested applications explicitly.\n\n|- Block comments are another way to annotate source. They do not nest. -|\n-- !export * selects local definitions. Use !export name for a smaller interface.\n-- An open import uses !import MANIFEST *; a reexport uses !export * from MANIFEST.\n-- Replace MANIFEST with the full identity from Explore libraries or the library guide.\n-- Reexports select the public interface; imports introduce names into scope.\n-- See Conversation for IO, and the docs for Maybe/Result/parser protocols.\n", + "sourceSha256": "d464c7964012c904cc322e957990be26c4172e11eea745ef5f75d4cd03a6635f", "hash": "86717d7f9d69fe730c6c6172c42c49895623aa070bf675e1b5f751adb30616c1", "mode": "pure", "haskellCases": [ @@ -554,8 +624,8 @@ }, "greet": { "demandCases": [], - "source": "!import 3dbb76ee4b350c161ed4f9c9f600034195b2fcf99202d46b8adcd25715a56f6b as IO\n!import 11791107bece9dac78ff92dcec75d11830921e359976f561a934b36f9e8910df as Text\n!export main\n\n-- Run main, then Send a line or choose End input in the terminal.\n-- Read and write Results are checked before continuing.\n-- Inspect the returned name as Text. Try changing the greeting.\ngreet writer [[\"line\" name _] _] = do IO.require\n Text.writeText writer \"Hello, \"\n Text.writeText writer name\n Text.writeText writer \"!\\n\"\n IO.pure name\ngreet writer [[\"eof\"] _] = IO.require\n (Text.writeText writer \"End of input.\\n\") (_ : IO.pure \"\")\ngreet _ _ = IO.fail \"Invalid line reply\"\n\nmain = IO.io\n do IO.require\n reader <- IO.stdin\n writer <- IO.stdout\n Text.writeText writer \"What is your name?\\n\"\n line <- Text.readLine (Text.lineCursor reader) 4096\n greet writer line\n", - "sourceSha256": "9db644e07fa3c58e12b19f74ba0eedadb7e5ad056fe9432ee7599f09fbdc309b", + "source": "!import 3dbb76ee4b350c161ed4f9c9f600034195b2fcf99202d46b8adcd25715a56f6b as IO\n!import 11791107bece9dac78ff92dcec75d11830921e359976f561a934b36f9e8910df as Text\n!export main\n\n-- A conversation: ask for a name, then greet the person who replies.\n-- Send a line or choose \"End input\" in the card's terminal.\n-- Read and write Results are checked before continuing.\n-- Return the name when IO finishes. Try changing the greeting.\ngreet writer [[\"line\" name _] _] = do IO.require\n Text.writeText writer \"Hello, \"\n Text.writeText writer name\n Text.writeText writer \"!\\n\"\n IO.pure name\ngreet writer [[\"eof\"] _] = IO.require\n (Text.writeText writer \"End of input.\\n\") (_ : IO.pure \"\")\ngreet _ _ = IO.fail \"Invalid line reply\"\n\nmain = IO.io\n do IO.require\n reader <- IO.stdin\n writer <- IO.stdout\n Text.writeText writer \"What is your name?\\n\"\n line <- Text.readLine (Text.lineCursor reader) 4096\n greet writer line\n", + "sourceSha256": "269f5ee9dcfa6cada4d18597fb35b2a1354535f204d38e3aab7d03621ad7adad", "hash": "b6f32449d85257fbd0a589d29ef05ea4a503c4a7e6105926d408b0551a34fd77", "mode": "io", "haskellCases": [ @@ -605,8 +675,8 @@ "result": "22021021020202021002210210210210202102100220210202021021021002210210202102021021000" } ], - "source": "!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 as B\n!export shape\n!export size\n!export same\n\n-- Inspect data OR compiled program trees, not source text.\n-- Try shape (t t t), size (t t t), or same (t t) (t t).\n-- Also try shape (x : x): functions are inspectable trees too.\n-- triage dispatches directly on Leaf, Stem and Fork without visiting children.\n-- Arithmetic and Booleans come from base; traversal is the subject of this demo.\nshape tree = B.triage \"Leaf\" (child : \"Stem\") (left right : \"Fork\") tree\n\n-- Count every constructor occurrence, including repeated shared branches.\n-- This is expanded tree size, not allocated nodes or source-code length.\ncount tree acc = B.triage\n (B.succ acc)\n (child : count child <| B.succ acc)\n (left right : B.succ acc |> count left |> count right)\n tree\n\nsize tree = count tree 0\n\nsame a b = B.triage\n (B.triage B.true (ignored : B.false) (ignored ignored2 : B.false) b)\n (x : B.triage B.false (y : same x y) (ignored ignored2 : B.false) b)\n (x y : B.triage B.false (ignored : B.false) (u v : B.and? (same x u) (same y v)) b)\n a\n", - "sourceSha256": "08c2b341d8947c9e46062dfa63f9d8810eb15620de499caf0891e313c5152cef", + "source": "!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 *\n!export shape\n!export size\n!export same\n\n-- Inspect data OR compiled program trees, not source text.\n-- Try shape (t t t), size (t t t), or same (t t) (t t).\n-- Also try shape (x : x): functions are inspectable trees too.\n-- triage dispatches directly on Leaf, Stem and Fork without visiting children.\n-- Arithmetic and Booleans come from base; traversal is the subject of this demo.\nshape tree = triage \"Leaf\" (child : \"Stem\") (left right : \"Fork\") tree\n\n-- Count every constructor occurrence, including repeated shared branches.\n-- This is expanded tree size, not allocated nodes or source-code length.\ncountNodes tree acc = triage\n (succ acc)\n (child : countNodes child <| succ acc)\n (left right : succ acc |> countNodes left |> countNodes right)\n tree\n\nsize tree = countNodes tree 0\n\nsame a b = triage\n (triage true (ignored : false) (ignored ignored2 : false) b)\n (x : triage false (y : same x y) (ignored ignored2 : false) b)\n (x y : triage false (ignored : false) (u v : and? (same x u) (same y v)) b)\n a\n", + "sourceSha256": "ac4bd61b5953c523e29a42387acfc6305075066edf6c709283b02de19c51025c", "hash": "493a125b649639c0df8348b4aeb3a455b7139cae0634ba114866c48813500bc5", "mode": "pure", "haskellCases": [ @@ -773,8 +843,8 @@ "result": "2210202020202021002202020202021002210210202020210210022020202102021021002210202020202102100220210210210202102100220210210210202102100221020210202021021002202021021020210210022020202020210022102020210202102100221021020202102102100220202020202100220210210210202102100221021021021020210210022020210202102102100220202020202100221020202020210210022020202020210022102102020202102100221020202020210210022021021021020210210022102102102102021021002202102102102021021002210202021020210210022102102020202102100221020202020210210022020210210202102100220202020202100220210202020210210022102020210210210210022020210202102102100221020210202021021002202020202021002202020210202100220202020210210022102021021020210022021020202102100221020210202102100221020210202102100221020202102021002202102102102021000" } ], - "source": "!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 *\n!import 7a24af56a9ce9b947cadcafd3a9da127b1507275bfac62b9fa55e27490b176dc as Binary\n!export main\n\n-- Validate an RGB list using the library's canonical-byte predicate.\n-- Try main [64 128 255], main [0 256 255], or main [0 1].\n-- Canonical encoding matters: redundant high zero bits are rejected too.\n-- List-shape errors are separate from invalid channels; checks short-circuit.\nmain (t _) = \"Expected a list.\"\nmain [_ | (t _)] = \"Expected a list.\"\nmain [_ | [_ | (t _)]] = \"Expected a list.\"\nmain [r g b] = case and? (Binary.binaryByte? r) (and? (Binary.binaryByte? g) (Binary.binaryByte? b)) of\n ^true = \"RGB triple accepted.\"\n _ = \"A channel is not a canonical byte (0-255).\"\nmain _ = \"Expected exactly three channels.\"\n", - "sourceSha256": "33da7deb793df280f59b47913b57562dbb2820525f19451ba5e4042219ef559e", + "source": "!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 *\n!import 7a24af56a9ce9b947cadcafd3a9da127b1507275bfac62b9fa55e27490b176dc *\n!export main\n\n-- Validate an RGB list using the library's canonical-byte predicate.\n-- Try result = main [64 128 255].\n-- Try changing its input to [0 256 255], then [0 1].\n-- Canonical encoding matters: redundant high zero bits are rejected too.\n-- List-shape errors are separate from invalid channels; checks short-circuit.\nmain (t _) = \"Expected a list.\"\nmain [_ | (t _)] = \"Expected a list.\"\nmain [_ | [_ | (t _)]] = \"Expected a list.\"\nmain [r g b] = case and? (binaryByte? r) (and? (binaryByte? g) (binaryByte? b)) of\n ^true = \"RGB triple accepted.\"\n _ = \"A channel is not a canonical byte (0-255).\"\nmain _ = \"Expected exactly three channels.\"\n", + "sourceSha256": "721a08c37746e7b982994a7981b8fcff7312d889dac3276e7e72ca8cc06ed87a", "hash": "b8c0e21c09f63511794cc62827a6b231e62a55f7e779022a1b740aaba29b0368", "mode": "pure", "haskellCases": [ @@ -861,8 +931,8 @@ "result": "222210210202021021021002202021020210210210022102020202021021002202021020210210210022102021020210210210022102102020210210210002022210202102020210210022021020202102102100220210202021021021002210210210210202102100220210202021021021000022221020210210202102100221020210202021021002210210202021021021002210210202021021021002210202020202102100221021021020202102100221020210202021021000202221021020202102021002210202102021021021002202102020202102100220202102021021021002210210210210202102100220202102021021021002210202020202102100220202102102021021002202020202021002210202020202102100220210210210202102100220202102020210210022020202020210022102102102020210210022021020202102102100221020202020210210022102021021020210210022102102020210210210022020202020210022102021021020210210022102021020210210210022102102020210210210022020210202102102100220202020202100220210202020210210022102021020202102100220202020202100221021020202021021002210202020202102100220210210210202102100221021021021020210210022021021021020210210022102020210202102100221021020202021021002210202020202102100220202102102021021002202020202021002202102102102021021002210202020202102100220202102021021021002210202102021021021002202102020210210210022102020202021021002202021021020210210022020202020210022021021021020210210022102021020210210210022102021021020210210022021020202021021002210202102020210210022021020202102102100221021020202102102100220210210210202100000" } ], - "source": "!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 *\n!export v1\n!export v2\n\n-- Shipping policy: arguments are destination, cart subtotal in cents, grams.\n-- Destinations: domestic, regional, international. Grams: 1..5000.\n-- Select v1/v2 with the same cart: domestic free shipping changes from $75 to $100.\n-- Otherwise domestic costs $4.99/$5.99, regional $9.99, international $19.99.\n-- Packages above 1kg add $2.50. Subtotals are bounded to $10,000.\n-- The bundle hash + export pins the policy used to reproduce a quote later.\n-- Records use [fieldName tag value]: 0 = text, 1 = natural, 2 = Boolean.\n-- Base supplies canonical-natural checks, arithmetic and Booleans.\n\nfailure message = [[\"status\" 0 \"error\"] [\"message\" 0 message]]\n\nbaseRate 1 \"domestic\" = 499\nbaseRate _ \"domestic\" = 599\nbaseRate _ \"regional\" = 999\nbaseRate _ _ = 1999\n\npaidRate version zone grams = case lte_? grams 1000 of\n ^true = baseRate version zone\n _ = add 250 <| baseRate version zone\n\nfreeThreshold 1 = 7500\nfreeThreshold _ = 10000\n\namount version zone subtotal grams = case and? (equal? zone \"domestic\") (lte_? (freeThreshold version) subtotal) of\n ^true = 0\n _ = paidRate version zone grams\n\nquote version zone subtotal grams = case and? (isNat? subtotal) (isNat? grams) of\n ^true = case or? (equal? zone \"domestic\") (or? (equal? zone \"regional\") (equal? zone \"international\")) of\n ^true = case and? (lte_? subtotal 1000000) (and? (lte_? 1 grams) (lte_? grams 5000)) of\n ^true = [[\"status\" 0 \"ok\"] [\"policyVersion\" 1 version]\n [\"destination\" 0 zone] [\"shippingCents\" 1 (amount version zone subtotal grams)]]\n _ = failure \"Subtotal must be <=1000000 cents; grams must be 1..5000.\"\n _ = failure \"Unknown destination.\"\n _ = failure \"Subtotal and grams must be canonical natural numbers.\"\n\nv1 zone subtotal grams = quote 1 zone subtotal grams\nv2 zone subtotal grams = quote 2 zone subtotal grams\n", - "sourceSha256": "dbbb53a9da0918c294f8d3b07768a756cae7d6beafb3c780caba3e6d4c0f7d4a", + "source": "!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 *\n!export v1\n!export v2\n\n-- Shipping policy: arguments are destination, cart subtotal in cents, grams.\n-- Destinations: domestic, regional, international. Grams: 1..5000.\n-- Try result = v1 \"domestic\" 8000 1500.\n-- Change v1 to v2: domestic free shipping changes from $75 to $100.\n-- Otherwise domestic costs $4.99/$5.99, regional $9.99, international $19.99.\n-- Packages above 1kg add $2.50. Subtotals are bounded to $10,000.\n-- The bundle hash + export pins the policy used to reproduce a quote later.\n-- Records use [fieldName tag value]: 0 = text, 1 = natural, 2 = Boolean.\n-- Base supplies canonical-natural checks, arithmetic and Booleans.\n\nfailure message = [[\"status\" 0 \"error\"] [\"message\" 0 message]]\n\nbaseRate 1 \"domestic\" = 499\nbaseRate _ \"domestic\" = 599\nbaseRate _ \"regional\" = 999\nbaseRate _ _ = 1999\n\npaidRate version zone grams = case lte_? grams 1000 of\n ^true = baseRate version zone\n _ = add 250 <| baseRate version zone\n\nfreeThreshold 1 = 7500\nfreeThreshold _ = 10000\n\namount version zone subtotal grams = case and? (equal? zone \"domestic\") (lte_? (freeThreshold version) subtotal) of\n ^true = 0\n _ = paidRate version zone grams\n\nquote version zone subtotal grams = case and? (isNat? subtotal) (isNat? grams) of\n ^true = case or? (equal? zone \"domestic\") (or? (equal? zone \"regional\") (equal? zone \"international\")) of\n ^true = case and? (lte_? subtotal 1000000) (and? (lte_? 1 grams) (lte_? grams 5000)) of\n ^true = [[\"status\" 0 \"ok\"] [\"policyVersion\" 1 version]\n [\"destination\" 0 zone] [\"shippingCents\" 1 (amount version zone subtotal grams)]]\n _ = failure \"Subtotal must be <=1000000 cents; grams must be 1..5000.\"\n _ = failure \"Unknown destination.\"\n _ = failure \"Subtotal and grams must be canonical natural numbers.\"\n\nv1 zone subtotal grams = quote 1 zone subtotal grams\nv2 zone subtotal grams = quote 2 zone subtotal grams\n", + "sourceSha256": "790497b064d8093290af112d8c1cc072c93da25aea4f288e87d7abefa9750e7f", "hash": "4aa9ce9f19dfce87ecb3f6bd0a2e881dafcbcd3d2e4b4075befbb378d80833ea", "mode": "pure", "haskellCases": [ @@ -1103,8 +1173,8 @@ "result": "2222102102020210210210022020210202102102100221020202020210210022020210202102102100221020210202102102100221021020202102102100020222102021020202102100220210202021021021002202102020210210210022102102102102021021002202102020210210210000222210202102102021021002210202102020210210022102102020210210210022102102020210210210022102020202021021002210210210202021021002210202102020210210002022202102020210210210022102021020202102100220210210202021021002210202102021021021002202102102102021021002202021020202102100221020210202021021002202021020202102100220202020202100221020210210202102100221020210202102102100221021020202102102100220202102021021021002202020202021002202102020202102100221020210202021021002202020202021002210202020202102100220202020202100221021020202021021002210202020202102100220210210210202102100221021021021020210210022021021021020210210022102020210202102100221021020202021021002210202020202102100220202102102021021002202020202021002202102020202021002210210210210202102100221021021021020210210022020210210202102100221020210202021021002210202020202102100220210210210202102100220210210210202100000" } ], - "source": "!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 *\n!import 3f70d95cf1bae0928ee89acd2a9e0cdd8287796efae32a9f08d33e3dd0149c55 as A\n!export vendorA\n!export vendorB\n\n-- Event adapter: two producers, one consumer-facing record schema.\n-- vendorA accepts [eventName customer totalCents]. eventName is purchase/refund.\n-- vendorB accepts [customer refunded totalCents]. refunded is a Boolean.\n-- Both return schemaVersion, eventType, customer and amountCents.\n-- Try the two exports: their presets describe the same order.created event.\n-- A producer can publish the adapter hash alongside its event schema.\n-- Records use [fieldName tag value]: 0 = text, 1 = natural, 2 = Boolean.\n-- Base supplies arithmetic/Booleans; A.hostString? checks Unicode scalar text.\n\nfailure message = [[\"status\" 0 \"error\"] [\"message\" 0 message]]\n\nnormalized event customer cents = case A.hostString? customer of\n ^true = case and? (isNat? cents) (lte_? cents 1000000) of\n ^true = [[\"schemaVersion\" 1 1] [\"eventType\" 0 event]\n [\"customer\" 0 customer] [\"amountCents\" 1 cents]]\n _ = failure \"amountCents must be a canonical natural <=1000000.\"\n _ = failure \"customer must be scalar text.\"\n\nfromA \"purchase\" customer cents = normalized \"order.created\" customer cents\nfromA \"refund\" customer cents = normalized \"order.refunded\" customer cents\nfromA _ _ _ = failure \"Unknown vendor A event.\"\n\nfromB customer ^true cents = normalized \"order.refunded\" customer cents\nfromB customer ^false cents = normalized \"order.created\" customer cents\nfromB _ _ _ = failure \"refunded must be a canonical Boolean.\"\n\n-- Preserve list-shape diagnostics without inspecting any field before dispatch.\nunpack handler (t _) = failure \"Expected a list.\"\nunpack handler [_ | (t _)] = failure \"Expected a list.\"\nunpack handler [_ | [_ | (t _)]] = failure \"Expected a list.\"\nunpack handler [a b c] = handler a b c\nunpack _ _ = failure \"Expected exactly three fields.\"\n\nvendorA values = unpack fromA values\nvendorB values = unpack fromB values\n", - "sourceSha256": "08a7857e4cb6ad2e4c1c9972f3ce05f26b810af8481ca974b91b1819bd4718e7", + "source": "!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 *\n!import 3f70d95cf1bae0928ee89acd2a9e0cdd8287796efae32a9f08d33e3dd0149c55 *\n!export vendorA\n!export vendorB\n\n-- Event adapter: two producers, one consumer-facing record schema.\n-- vendorA accepts [eventName customer totalCents]. eventName is purchase/refund.\n-- vendorB accepts [customer refunded totalCents]. refunded is a Boolean.\n-- Both return schemaVersion, eventType, customer and amountCents.\n-- Try result = vendorA [\"purchase\" \"James\" 2499].\n-- Try result = vendorB [\"James\" false 2499] to describe the same event.\n-- A producer can publish the adapter hash alongside its event schema.\n-- Records use [fieldName tag value]: 0 = text, 1 = natural, 2 = Boolean.\n-- Base supplies arithmetic/Booleans; hostString? checks Unicode scalar text.\n\nfailure message = [[\"status\" 0 \"error\"] [\"message\" 0 message]]\n\nnormalized event customer cents = case hostString? customer of\n ^true = case and? (isNat? cents) (lte_? cents 1000000) of\n ^true = [[\"schemaVersion\" 1 1] [\"eventType\" 0 event]\n [\"customer\" 0 customer] [\"amountCents\" 1 cents]]\n _ = failure \"amountCents must be a canonical natural <=1000000.\"\n _ = failure \"customer must be scalar text.\"\n\nfromA \"purchase\" customer cents = normalized \"order.created\" customer cents\nfromA \"refund\" customer cents = normalized \"order.refunded\" customer cents\nfromA _ _ _ = failure \"Unknown vendor A event.\"\n\nfromB customer ^true cents = normalized \"order.refunded\" customer cents\nfromB customer ^false cents = normalized \"order.created\" customer cents\nfromB _ _ _ = failure \"refunded must be a canonical Boolean.\"\n\n-- Preserve list-shape diagnostics without inspecting any field before dispatch.\nunpack handler (t _) = failure \"Expected a list.\"\nunpack handler [_ | (t _)] = failure \"Expected a list.\"\nunpack handler [_ | [_ | (t _)]] = failure \"Expected a list.\"\nunpack handler [a b c] = handler a b c\nunpack _ _ = failure \"Expected exactly three fields.\"\n\nvendorA values = unpack fromA values\nvendorB values = unpack fromB values\n", + "sourceSha256": "3d086d7b0dba582e95e28515747c6d80cd4ab0af3c825af30fd759b1788527e3", "hash": "8b51693c63658d59c8083ec1c95cf1f6565ce5799791f540e92caf508613c9fb", "mode": "pure", "haskellCases": [ @@ -1355,8 +1425,8 @@ }, "registration": { "demandCases": [], - "source": "!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 *\n!import 3f70d95cf1bae0928ee89acd2a9e0cdd8287796efae32a9f08d33e3dd0149c55 as A\n!export main\n\n-- Registration validator: name, email, age, termsAccepted are separate arguments.\n-- Returns one field-specific message per input, plus an accepted Boolean.\n-- Name must be nonempty scalar text. Age must be a natural from 18 to 120.\n-- Email uses a deliberately small rule: nonempty text containing @ and a dot.\n-- Terms must be the canonical true Boolean. This is a teaching policy, not SMTP validation.\n-- Browser and server can pin this same executable instead of duplicating rules.\n-- Records use [fieldName tag value]: 0 = text, 1 = natural, 2 = Boolean.\n-- Base supplies arithmetic/Booleans; A.hostString? checks Unicode scalar text.\n\nnameMessage name = case A.hostString? name of\n ^true = case name of\n [] = \"Name is required.\"\n _ = \"\"\n _ = \"Name must be scalar text.\"\n\nemailMessage email = case A.hostString? email of\n ^true = case and? (contains? \"@\" email) (contains? \".\" email) of\n ^true = \"\"\n _ = \"Email must contain @ and a dot.\"\n _ = \"Email must be scalar text.\"\n\nageMessage age = case isNat? age of\n ^true = case and? (lte_? 18 age) (lte_? age 120) of\n ^true = \"\"\n _ = \"Age must be from 18 to 120.\"\n _ = \"Age must be a canonical natural.\"\n\ntermsMessage ^true = \"\"\ntermsMessage _ = \"Accept the terms.\"\n\nreport name email age terms = [\n [\"accepted\" 2 (and? (equal? name \"\") (and? (equal? email \"\") (and? (equal? age \"\") (equal? terms \"\"))))]\n [\"nameError\" 0 name] [\"emailError\" 0 email]\n [\"ageError\" 0 age] [\"termsError\" 0 terms]]\n\nmain name email age terms = report (nameMessage name) (emailMessage email) (ageMessage age) (termsMessage terms)\n", - "sourceSha256": "b15d4e60a34d839b06c813fa439ea063abc4a771bc2d0dd24cd928eb21ca7e29", + "source": "!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 *\n!import 3f70d95cf1bae0928ee89acd2a9e0cdd8287796efae32a9f08d33e3dd0149c55 *\n!export main\n\n-- Registration validator: name, email, age, termsAccepted are separate arguments.\n-- Returns one field-specific message per input, plus an accepted Boolean.\n-- Try result = main \"James\" \"james@example.org\" 21 true.\n-- Try changing the age to 17 or the email to \"bad\" to see field messages.\n-- Name must be nonempty scalar text. Age must be a natural from 18 to 120.\n-- Email uses a deliberately small rule: nonempty text containing @ and a dot.\n-- Terms must be the canonical true Boolean. This is a teaching policy, not SMTP validation.\n-- Browser and server can pin this same executable instead of duplicating rules.\n-- Records use [fieldName tag value]: 0 = text, 1 = natural, 2 = Boolean.\n-- Base supplies arithmetic/Booleans; hostString? checks Unicode scalar text.\n\nnameMessage name = case hostString? name of\n ^true = case name of\n [] = \"Name is required.\"\n _ = \"\"\n _ = \"Name must be scalar text.\"\n\nemailMessage email = case hostString? email of\n ^true = case and? (contains? \"@\" email) (contains? \".\" email) of\n ^true = \"\"\n _ = \"Email must contain @ and a dot.\"\n _ = \"Email must be scalar text.\"\n\nageMessage age = case isNat? age of\n ^true = case and? (lte_? 18 age) (lte_? age 120) of\n ^true = \"\"\n _ = \"Age must be from 18 to 120.\"\n _ = \"Age must be a canonical natural.\"\n\ntermsMessage ^true = \"\"\ntermsMessage _ = \"Accept the terms.\"\n\nreport name email age terms = [\n [\"accepted\" 2 (and? (equal? name \"\") (and? (equal? email \"\") (and? (equal? age \"\") (equal? terms \"\"))))]\n [\"nameError\" 0 name] [\"emailError\" 0 email]\n [\"ageError\" 0 age] [\"termsError\" 0 terms]]\n\nmain name email age terms = report (nameMessage name) (emailMessage email) (ageMessage age) (termsMessage terms)\n", + "sourceSha256": "c91388231dfc9a04da02e750cfcbd873947f6cf761a59352446000421c7806dd", "hash": "679733e17a86cc5e3f5282ca824cad4c9c0febd260e51f6a87e16dd14c02c45a", "mode": "pure", "haskellCases": [ @@ -1436,8 +1506,8 @@ }, "migration": { "demandCases": [], - "source": "!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 *\n!import 3f70d95cf1bae0928ee89acd2a9e0cdd8287796efae32a9f08d33e3dd0149c55 as A\n!export v1\n!export v2\n\n-- Archived profile v0: [name email]. v1: adds schemaVersion and displayName.\n-- v2: also supplies marketingOptIn=false; existing data is preserved.\n-- Select v1/v2 with the same input to compare the two target schemas.\n-- A migration receipt can record the input identity, executable hash/export,\n-- output identity and wire schema. Running this demo stays entirely local.\n-- Records use [fieldName tag value]: 0 = text, 1 = natural, 2 = Boolean.\n-- Base supplies arithmetic/Booleans; A.hostString? checks Unicode scalar text.\n\nfailure message = [[\"status\" 0 \"error\"] [\"message\" 0 message]]\n\nmigrate version name email = case and? (A.hostString? name) (A.hostString? email) of\n ^true = case version of\n 1 = [[\"schemaVersion\" 1 1] [\"displayName\" 0 name] [\"email\" 0 email]]\n _ = [[\"schemaVersion\" 1 2] [\"displayName\" 0 name] [\"email\" 0 email]\n [\"marketingOptIn\" 2 false]]\n _ = failure \"Name and email must be scalar text.\"\n\nprofile _ (t _) = failure \"Expected a list.\"\nprofile _ [_ | (t _)] = failure \"Expected a list.\"\nprofile version [name email] = migrate version name email\nprofile _ _ = failure \"Expected [name email].\"\n\nv1 values = profile 1 values\nv2 values = profile 2 values\n", - "sourceSha256": "a9270baef5cb5102c895c9f23434ebc82c87eebfa16c25cb415b06b4636a279e", + "source": "!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 *\n!import 3f70d95cf1bae0928ee89acd2a9e0cdd8287796efae32a9f08d33e3dd0149c55 *\n!export v1\n!export v2\n\n-- Archived profile v0: [name email]. v1: adds schemaVersion and displayName.\n-- v2: also supplies marketingOptIn=false; existing data is preserved.\n-- Try result = v1 [\"James\" \"james@example.org\"].\n-- Change v1 to v2 to compare the two target schemas.\n-- A migration receipt can record the input identity, executable hash/export,\n-- output identity and wire schema. Running this demo stays entirely local.\n-- Records use [fieldName tag value]: 0 = text, 1 = natural, 2 = Boolean.\n-- Base supplies arithmetic/Booleans; hostString? checks Unicode scalar text.\n\nfailure message = [[\"status\" 0 \"error\"] [\"message\" 0 message]]\n\nmigrate version name email = case and? (hostString? name) (hostString? email) of\n ^true = case version of\n 1 = [[\"schemaVersion\" 1 1] [\"displayName\" 0 name] [\"email\" 0 email]]\n _ = [[\"schemaVersion\" 1 2] [\"displayName\" 0 name] [\"email\" 0 email]\n [\"marketingOptIn\" 2 false]]\n _ = failure \"Name and email must be scalar text.\"\n\nprofile _ (t _) = failure \"Expected a list.\"\nprofile _ [_ | (t _)] = failure \"Expected a list.\"\nprofile version [name email] = migrate version name email\nprofile _ _ = failure \"Expected [name email].\"\n\nv1 values = profile 1 values\nv2 values = profile 2 values\n", + "sourceSha256": "239e45bde8f461360a016bfbcca3111dc72da3b7dc237736d05b12dc6018e9f8", "hash": "730aeb8203ff504b7483999a6477a292b10871f82e94a77d3d5f7b49ecc3b6b3", "mode": "pure", "haskellCases": [ diff --git a/ext/demo-site/demos/arithmetic.tri b/ext/demo-site/demos/arithmetic.tri @@ -1,19 +1,13 @@ -!import 7f87549d587fa8c3be88af0ea988db3adcc3b35f1ffb48973553491b075614db as Math +!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 * !export * --- Add the prices of two items. -total = Math.add 5 7 --- Evaluate total with Natural number. --- Result: 12. --- Add import supplies the library identity; Math is our short name. - --- Function calls use spaces: Math.multiply takes two numbers. -product = Math.multiply 5 7 +-- Let's do some arithmetic! Our import supplies add and mul (multiply). +-- Function calls use spaces between the function and its inputs. +total = add 5 7 +multiplied = mul 5 7 -- Parentheses make one calculation the input to another. -grouped = Math.multiply (Math.add 5 7) 2 +grouped = mul (add 5 7) 2 --- Try product (35), then grouped (24), with Natural number. --- Exercise: change total to add the prices of two things you would buy. --- Numbers here are whole numbers starting at zero. +-- Try changing the numbers to calculate the cost of two things you'd buy. -- Next: Your first function. diff --git a/ext/demo-site/demos/choices.tri b/ext/demo-site/demos/choices.tri @@ -1,15 +1,16 @@ -!export message +!export * --- Choose an answer by matching an input. +-- Choose a message by matching an input. message 0 = "Your basket is empty." message 1 = "You have one item." message _ = "You have several items." --- Evaluate message 0 with Text: Your basket is empty. -- These equations are tried from top to bottom. -- _ matches any remaining input: this is the fallback. --- Try message 1, then message 4, using Text. +emptyBasket = message 0 +oneItem = message 1 +fullBasket = message 4 --- Exercise: add a message for 2 items before the fallback equation. +-- Try adding a message for 2 items before the fallback equation. -- Keep the equations together so they define one function. --- Next: try a Practical Example, or browse Language reference. +-- Next: a Practical Example, or Language reference. diff --git a/ext/demo-site/demos/events.tri b/ext/demo-site/demos/events.tri @@ -1,5 +1,5 @@ !import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 * -!import 3f70d95cf1bae0928ee89acd2a9e0cdd8287796efae32a9f08d33e3dd0149c55 as A +!import 3f70d95cf1bae0928ee89acd2a9e0cdd8287796efae32a9f08d33e3dd0149c55 * !export vendorA !export vendorB @@ -7,14 +7,15 @@ -- vendorA accepts [eventName customer totalCents]. eventName is purchase/refund. -- vendorB accepts [customer refunded totalCents]. refunded is a Boolean. -- Both return schemaVersion, eventType, customer and amountCents. --- Try the two exports: their presets describe the same order.created event. +-- Try result = vendorA ["purchase" "James" 2499]. +-- Try result = vendorB ["James" false 2499] to describe the same event. -- A producer can publish the adapter hash alongside its event schema. -- Records use [fieldName tag value]: 0 = text, 1 = natural, 2 = Boolean. --- Base supplies arithmetic/Booleans; A.hostString? checks Unicode scalar text. +-- Base supplies arithmetic/Booleans; hostString? checks Unicode scalar text. failure message = [["status" 0 "error"] ["message" 0 message]] -normalized event customer cents = case A.hostString? customer of +normalized event customer cents = case hostString? customer of ^true = case and? (isNat? cents) (lte_? cents 1000000) of ^true = [["schemaVersion" 1 1] ["eventType" 0 event] ["customer" 0 customer] ["amountCents" 1 cents]] diff --git a/ext/demo-site/demos/functions.tri b/ext/demo-site/demos/functions.tri @@ -1,19 +1,16 @@ -!import 7f87549d587fa8c3be88af0ea988db3adcc3b35f1ffb48973553491b075614db as Math +!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 * !export * --- Reuse a calculation with different inputs. -double number = Math.multiply number 2 --- Evaluate double 6 with Natural number. --- Result: 12. --- Add import supplies Math, including multiplication. +-- Give a calculation a name so we can reuse it with different inputs. +double number = mul number 2 -- number is a parameter: each call supplies its value. --- Try double 9: the same definition now returns 18. +sixDoubled = double 6 +nineDoubled = double 9 --- A function can also take more than one input. -add left right = Math.add left right --- Try add 4 7 with Natural number: the result is 11. +-- A function can take more than one input. +rectangle width height = mul width height +area = rectangle 4 7 --- Exercise: change double to multiply by 3, and give it the name triple. --- Then evaluate triple 6. Update the expression to use your new name. +-- Try writing triple and using it in place of double. -- Next: Working with lists. diff --git a/ext/demo-site/demos/greet.tri b/ext/demo-site/demos/greet.tri @@ -2,9 +2,10 @@ !import 11791107bece9dac78ff92dcec75d11830921e359976f561a934b36f9e8910df as Text !export main --- Run main, then Send a line or choose End input in the terminal. +-- A conversation: ask for a name, then greet the person who replies. +-- Send a line or choose "End input" in the card's terminal. -- Read and write Results are checked before continuing. --- Inspect the returned name as Text. Try changing the greeting. +-- Return the name when IO finishes. Try changing the greeting. greet writer [["line" name _] _] = do IO.require Text.writeText writer "Hello, " Text.writeText writer name diff --git a/ext/demo-site/demos/language-reference.tri b/ext/demo-site/demos/language-reference.tri @@ -5,9 +5,9 @@ -- The open base import supplies contract helpers and unqualified names. -- Math is a qualified import: its names start with Math. -- A syntax sampler: independent examples, rather than one application. --- Start with answer / Natural number: 42. Choose a definition for its preset. +-- Start with answer: its value is 42. -- Full contracts: follow For AI assistants in the footer to Language features. --- Each comment below gives an expression and its interpretation. +-- Comments give sample calls and interpretations; name a call to try it. -- Names, lambdas, named parameters, application and grouping. identity = (x : x) @@ -27,7 +27,7 @@ literals = ["hello" 42 [1 2 3]] addTwo = Math.add 2 -- addTwo 5 / Natural number -> 7. --- Ordered equations: literals, a qualified pin, exact lists, cons and wildcard. +-- Ordered equations: literals, a pin, exact lists, cons and wildcard. -- A pin (^true) compares against an existing value. describe 7 = "seven" describe "hi" = "greeting" @@ -89,6 +89,6 @@ sequenced input = do (value next : next value) |- Block comments are another way to annotate source. They do not nest. -| -- !export * selects local definitions. Use !export name for a smaller interface. -- An open import uses !import MANIFEST *; a reexport uses !export * from MANIFEST. --- Replace MANIFEST with the full identity from Add import or the library guide. +-- Replace MANIFEST with the full identity from Explore libraries or the library guide. -- Reexports select the public interface; imports introduce names into scope. -- See Conversation for IO, and the docs for Maybe/Result/parser protocols. diff --git a/ext/demo-site/demos/lists.tri b/ext/demo-site/demos/lists.tri @@ -1,17 +1,10 @@ -!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 as List -!export count -!export total +!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 * +!export * --- Collect values in a list. +-- Collect values in a list. Spaces separate items; [] is the empty list. numbers = [2 4 6] -count = List.length numbers -total = List.sum numbers +itemCount = length numbers +total = sum numbers --- Evaluate count with Natural number: the result is 3. --- Spaces separate items. [] is the empty list. --- Add import supplies List, including length and sum. - --- Try total with Natural number: the result is 12. --- Exercise: add 8 to numbers. Check count (4) and total (20). --- Then try numbers = []: both results become 0. +-- Try adding another item to numbers, then try numbers = []. -- Next: Transforming a list. diff --git a/ext/demo-site/demos/microscope.tri b/ext/demo-site/demos/microscope.tri @@ -1,4 +1,4 @@ -!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 as B +!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 * !export shape !export size !export same @@ -8,20 +8,20 @@ -- Also try shape (x : x): functions are inspectable trees too. -- triage dispatches directly on Leaf, Stem and Fork without visiting children. -- Arithmetic and Booleans come from base; traversal is the subject of this demo. -shape tree = B.triage "Leaf" (child : "Stem") (left right : "Fork") tree +shape tree = triage "Leaf" (child : "Stem") (left right : "Fork") tree -- Count every constructor occurrence, including repeated shared branches. -- This is expanded tree size, not allocated nodes or source-code length. -count tree acc = B.triage - (B.succ acc) - (child : count child <| B.succ acc) - (left right : B.succ acc |> count left |> count right) +countNodes tree acc = triage + (succ acc) + (child : countNodes child <| succ acc) + (left right : succ acc |> countNodes left |> countNodes right) tree -size tree = count tree 0 +size tree = countNodes tree 0 -same a b = B.triage - (B.triage B.true (ignored : B.false) (ignored ignored2 : B.false) b) - (x : B.triage B.false (y : same x y) (ignored ignored2 : B.false) b) - (x y : B.triage B.false (ignored : B.false) (u v : B.and? (same x u) (same y v)) b) +same a b = triage + (triage true (ignored : false) (ignored ignored2 : false) b) + (x : triage false (y : same x y) (ignored ignored2 : false) b) + (x y : triage false (ignored : false) (u v : and? (same x u) (same y v)) b) a diff --git a/ext/demo-site/demos/migration.tri b/ext/demo-site/demos/migration.tri @@ -1,19 +1,20 @@ !import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 * -!import 3f70d95cf1bae0928ee89acd2a9e0cdd8287796efae32a9f08d33e3dd0149c55 as A +!import 3f70d95cf1bae0928ee89acd2a9e0cdd8287796efae32a9f08d33e3dd0149c55 * !export v1 !export v2 -- Archived profile v0: [name email]. v1: adds schemaVersion and displayName. -- v2: also supplies marketingOptIn=false; existing data is preserved. --- Select v1/v2 with the same input to compare the two target schemas. +-- Try result = v1 ["James" "james@example.org"]. +-- Change v1 to v2 to compare the two target schemas. -- A migration receipt can record the input identity, executable hash/export, -- output identity and wire schema. Running this demo stays entirely local. -- Records use [fieldName tag value]: 0 = text, 1 = natural, 2 = Boolean. --- Base supplies arithmetic/Booleans; A.hostString? checks Unicode scalar text. +-- Base supplies arithmetic/Booleans; hostString? checks Unicode scalar text. failure message = [["status" 0 "error"] ["message" 0 message]] -migrate version name email = case and? (A.hostString? name) (A.hostString? email) of +migrate version name email = case and? (hostString? name) (hostString? email) of ^true = case version of 1 = [["schemaVersion" 1 1] ["displayName" 0 name] ["email" 0 email]] _ = [["schemaVersion" 1 2] ["displayName" 0 name] ["email" 0 email] diff --git a/ext/demo-site/demos/names-values.tri b/ext/demo-site/demos/names-values.tri @@ -1,18 +1,19 @@ -!export greeting -!export answer +!export * --- Give a value a name. +-- Welcome to tricu! Anything after -- on a line is a comment. +-- tricu is a lazy functional language based on tree calculus. + +-- Click or tap underlined definition names to inspect their values. +-- Function calls can have names too; we'll try that in "Your first function". +-- Edits compile after a pause. IO programs offer "Run IO" inside their card. + +-- tricu offers string literals, greeting = "Hello, world!" --- Evaluate greeting with Text. --- Result: Hello, world! --- Try changing the words, then Evaluate again. --- Numbers can have names too. +-- natural number literals (whole numbers starting at zero), answer = 42 --- Choose answer in Definition. --- It selects Natural number; the result is 42. --- Exercise: change answer to your favourite number and evaluate it. --- The !export lines make our names available to the playground. --- Edits compile after a pause; Evaluate runs the expression. --- Next: Doing arithmetic. +-- list literals, +greetAndAnswer = [greeting answer] + +-- and more! Choose "Doing arithmetic" from the "Program" dropdown next. diff --git a/ext/demo-site/demos/registration.tri b/ext/demo-site/demos/registration.tri @@ -1,23 +1,25 @@ !import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 * -!import 3f70d95cf1bae0928ee89acd2a9e0cdd8287796efae32a9f08d33e3dd0149c55 as A +!import 3f70d95cf1bae0928ee89acd2a9e0cdd8287796efae32a9f08d33e3dd0149c55 * !export main -- Registration validator: name, email, age, termsAccepted are separate arguments. -- Returns one field-specific message per input, plus an accepted Boolean. +-- Try result = main "James" "james@example.org" 21 true. +-- Try changing the age to 17 or the email to "bad" to see field messages. -- Name must be nonempty scalar text. Age must be a natural from 18 to 120. -- Email uses a deliberately small rule: nonempty text containing @ and a dot. -- Terms must be the canonical true Boolean. This is a teaching policy, not SMTP validation. -- Browser and server can pin this same executable instead of duplicating rules. -- Records use [fieldName tag value]: 0 = text, 1 = natural, 2 = Boolean. --- Base supplies arithmetic/Booleans; A.hostString? checks Unicode scalar text. +-- Base supplies arithmetic/Booleans; hostString? checks Unicode scalar text. -nameMessage name = case A.hostString? name of +nameMessage name = case hostString? name of ^true = case name of [] = "Name is required." _ = "" _ = "Name must be scalar text." -emailMessage email = case A.hostString? email of +emailMessage email = case hostString? email of ^true = case and? (contains? "@" email) (contains? "." email) of ^true = "" _ = "Email must contain @ and a dot." diff --git a/ext/demo-site/demos/rgb.tri b/ext/demo-site/demos/rgb.tri @@ -1,15 +1,16 @@ !import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 * -!import 7a24af56a9ce9b947cadcafd3a9da127b1507275bfac62b9fa55e27490b176dc as Binary +!import 7a24af56a9ce9b947cadcafd3a9da127b1507275bfac62b9fa55e27490b176dc * !export main -- Validate an RGB list using the library's canonical-byte predicate. --- Try main [64 128 255], main [0 256 255], or main [0 1]. +-- Try result = main [64 128 255]. +-- Try changing its input to [0 256 255], then [0 1]. -- Canonical encoding matters: redundant high zero bits are rejected too. -- List-shape errors are separate from invalid channels; checks short-circuit. main (t _) = "Expected a list." main [_ | (t _)] = "Expected a list." main [_ | [_ | (t _)]] = "Expected a list." -main [r g b] = case and? (Binary.binaryByte? r) (and? (Binary.binaryByte? g) (Binary.binaryByte? b)) of +main [r g b] = case and? (binaryByte? r) (and? (binaryByte? g) (binaryByte? b)) of ^true = "RGB triple accepted." _ = "A channel is not a canonical byte (0-255)." main _ = "Expected exactly three channels." diff --git a/ext/demo-site/demos/shipping.tri b/ext/demo-site/demos/shipping.tri @@ -4,7 +4,8 @@ -- Shipping policy: arguments are destination, cart subtotal in cents, grams. -- Destinations: domestic, regional, international. Grams: 1..5000. --- Select v1/v2 with the same cart: domestic free shipping changes from $75 to $100. +-- Try result = v1 "domestic" 8000 1500. +-- Change v1 to v2: domestic free shipping changes from $75 to $100. -- Otherwise domestic costs $4.99/$5.99, regional $9.99, international $19.99. -- Packages above 1kg add $2.50. Subtotals are bounded to $10,000. -- The bundle hash + export pins the policy used to reproduce a quote later. diff --git a/ext/demo-site/demos/transform-list.tri b/ext/demo-site/demos/transform-list.tri @@ -1,18 +1,14 @@ -!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 as List -!import 7f87549d587fa8c3be88af0ea988db3adcc3b35f1ffb48973553491b075614db as Math -!export total -!export double +!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 * +!export * --- Apply the same function to every item. -double number = Math.multiply number 2 +-- Functions are values too. We can pass one to another function. +double number = mul number 2 numbers = [2 4 6] --- map takes a function, then a list. Here it produces [4 8 12]. -doubled = List.map double numbers -total = List.sum doubled --- Evaluate total with Natural number: the result is 24. --- Add import supplies List and Math. +-- map takes a function, then a list, and applies it to every item. +doubled = map double numbers +-- map builds a new list; the original numbers are unchanged. +total = sum doubled --- Try double 5 with Natural number: the result is 10. --- Exercise: make double multiply by 3. The new total should be 36. +-- Try multiplying by 3 instead. -- Next: Making a choice. diff --git a/ext/demo-site/dist/app.js b/ext/demo-site/dist/app.js @@ -5,7 +5,7 @@ import { exampleGroups, exampleGroup, libraryLabel } from './program-catalogue.j import { importPicker } from './import-picker.js'; import { localModulesUI } from './local-modules-ui.js'; import { tokenize, paintSyntax } from './syntax.js'; -import { backend, backends, runtimeStats } from './runtime-backends.js'; +import { backends, runtimeStats } from './runtime-backends.js'; import { setupInspector } from './inspector.js'; import { ActiveClock } from './clock.js'; import { PACKAGE_LOAD_MS, operationFailure, operationTimeout } from './execution.js'; @@ -22,10 +22,10 @@ function el(id) { const sourceEditor = new SourceEditor(el('source-editor')); const compileButton = el('compile'), restoreButton = el('restore-source'); const downloadButton = el('download-bundle'), exportModuleButton = el('export-compiled-module'); -const backendPicker = el('runtime-backend'), programPicker = el('program-picker'); +const programPicker = el('program-picker'); const versionPicker = el('package-version'), hashInput = el('bundle-hash'); const stopButton = el('stop'), shareButton = el('share'); -let selectedBackend = backend(backendPicker.value); +const selectedBackend = 'haskell-wasm'; const workerURL = () => new URL(backends[selectedBackend].worker, import.meta.url); let draft = false, scratch = false, compiled = false, busy = false, loaded = false; let originalSource, compiledSource, compiledRevision; @@ -96,7 +96,7 @@ const definitions = definitionInspector(sourceEditor, (id, name, output) => { deadline.start(); }, }); -const imports = importPicker({ source: sourceEditor, backend: () => backendPicker.value, +const imports = importPicker({ source: sourceEditor, backend: () => selectedBackend, available: () => !busy, editable: () => !busy && !sourceEditor.dom.hidden && !sourceEditor.isReadOnly, tryLibrary: (library, definition) => { preferredExport = definition; loadBundle(library); }, local: () => localModules.entries(), inputs: () => localModules.inputs() }); @@ -105,7 +105,6 @@ function ready() { return !!worker && (draft ? compiled && sourceEditor.getSourc function controls(working) { busy = working; localModules.disable(busy); - backendPicker.disabled = busy; programPicker.disabled = busy || !entries.length; versionPicker.disabled = hashInput.disabled = busy; el('load-release').disabled = busy || !programPackage(programPicker.value); @@ -119,7 +118,6 @@ function controls(working) { downloadButton.disabled = busy || draft || downloadHash() === undefined; sourceEditor.setAvailability(busy && operation !== 'compiling', originalSource === undefined && !draft); const editable = !sourceEditor.dom.hidden && (draft || originalSource !== undefined); - el('playground-intro').hidden = selected?.id !== 'names-values' || !editable; el('source-title').textContent = editable ? 'Source' : sourceAvailable ? 'Read-only source' : loaded ? 'Compiled package' : 'Package information'; el('source-availability').hidden = editable || !loaded; el('source-availability').textContent = 'Browse stored definitions below, or import this module into your own source. Inspection does not execute IO.'; @@ -738,12 +736,6 @@ function compileDraft() { } exportModuleButton.onclick = () => { if (busy || !draft || !ready() || !worker) return; operation = 'exporting'; controls(true); deadline.reset(Infinity); deadline.start(); el('status').textContent = 'Exporting module'; el('compiled-module-status').textContent = 'Observing public trees; Stop is available…'; worker.postMessage({ type: 'export-module' }); }; -backendPicker.onchange = () => { selectedBackend = backend(backendPicker.value); editRevision++; definitions.reset(); dispose(); if (draft) { - el('status').textContent = 'Changes pending'; - queueCompile(0); -} -else if (selection) - newWorker(true); }; const examples = { identity: 'id = (x : x)\nanswer = id 42', contracts: 'id x =@nat? x\nanswer = id 42', diff --git a/ext/demo-site/index.html b/ext/demo-site/index.html @@ -42,7 +42,6 @@ <section id="editor" class="workspace" aria-label="tricu editor"> <div class="editor-toolbar" data-inspector-background> <div class="toolbar-selection"><label for="program-picker">Program</label><select id="program-picker" disabled><option value="">Loading Names and values…</option></select></div> - <div class="toolbar-runtime"><label for="runtime-backend">Runtime</label><select id="runtime-backend"><option value="haskell-wasm" selected>Haskell</option></select></div> <div class="toolbar-actions"><button id="add-import" disabled>Explore libraries…</button><button id="stop" disabled hidden>Stop</button><button id="reload-playground" title="Reload the page to restart all runtimes. Local drafts, output and opened modules are cleared.">Reload Playground</button><button id="settings-toggle" aria-controls="settings-panel" aria-expanded="false">Settings</button></div> <span id="status" role="status">Loading Names and values</span> </div> @@ -50,7 +49,6 @@ <div class="editor-main" data-inspector-background> <section class="source-panel" aria-labelledby="source-title"> <div class="source-heading"><h2 id="source-title">Source</h2><span id="source-label">tricu / stable</span></div> - <p id="playground-intro" class="hint" hidden><strong>Try the Playground:</strong> click an underlined definition name to inspect its compiled tree and choose an interpretation. Write calls as source definitions. IO programs offer an explicit <strong>Run IO</strong> button inside their card.</p> <p id="authorship">Published package</p><p id="source-availability" class="hint" hidden></p> <label class="sr-only" id="source-editor-label">tricu source (up to 16,384 characters)</label> <div id="source-editor" hidden></div> diff --git a/ext/demo-site/packages.json b/ext/demo-site/packages.json @@ -5398,6 +5398,54 @@ "manifest": "0713754ddf34e447f3581fc105226da560f0c6533f1e08b666057e061866b600", "title": "Profile migrations", "demo": "migration" + }, + { + "name": "names-values", + "version": "0.10.0", + "hash": "4116a3702923898d5fafc8c855fbea38a7fd740bdb15667ef53fec7268b41381", + "manifest": "33883463035f1686af7c83e9bce555f3553bd8ae52c3c82aeed6b8a64815e4e4", + "title": "Names and values", + "demo": "names-values" + }, + { + "name": "arithmetic", + "version": "0.10.0", + "hash": "ac504497f656b84b7ae1e7f9a00086c76d05686df3eee2c5612a8c7f51fbf141", + "manifest": "acf0df87effccaeb162a8f8bae028905c86a5e21c43ebde0d3cd800662868675", + "title": "Doing arithmetic", + "demo": "arithmetic" + }, + { + "name": "functions", + "version": "0.10.0", + "hash": "6c85d85a51c0c3ed8fb9adc1a5c81c18313174b3f72371933e432f3f895c1667", + "manifest": "e0c6951111db1a42dfe2cdff24855bf506c3d09446ed9cdc494b80f5b9865839", + "title": "Your first function", + "demo": "functions" + }, + { + "name": "lists", + "version": "0.10.0", + "hash": "8eb72ff6766fb762614c6dca9026ebda78d06f5e8f78e48cc29b195ba2154b21", + "manifest": "97b67228a0ec951bb7ff66a3b65eca22fadaabbd21784f20e193e75ddd191807", + "title": "Working with lists", + "demo": "lists" + }, + { + "name": "transform-list", + "version": "0.10.0", + "hash": "639c8d3959320ef99d9416257905f189519a90ea4b315cdbb0485a386e46191c", + "manifest": "f6f657502414339432d9cea9f430b444818f184d04fb0096f5bcf8093ac7f77a", + "title": "Transforming a list", + "demo": "transform-list" + }, + { + "name": "choices", + "version": "0.10.0", + "hash": "dd0931401769303bf2e1ce08c51e7778779ccc9643ca8315c256c0542eedff7a", + "manifest": "3e73066099e6a6b3ba82c0e3e44ec05c842af80fad56697e908d03a1a8a4dffa", + "title": "Making a choice", + "demo": "choices" } ] } diff --git a/ext/demo-site/reference-input/getting-started.md b/ext/demo-site/reference-input/getting-started.md @@ -185,5 +185,5 @@ repeatedly errors after an idle period. This reloads the whole page, including all WASM sessions. It stays available while other work is running. If you have a local draft or opened modules, confirm before discarding them; cancel to copy your work first. Reload also clears inspection results, IO state and the local-module collection, -resets the runtime selector, and loads the normal URL/default selection without +and loads the normal URL/default selection without automatically evaluating it. diff --git a/ext/demo-site/src/app.ts b/ext/demo-site/src/app.ts @@ -5,7 +5,7 @@ import { exampleGroups, exampleGroup, libraryLabel } from './program-catalogue.j import { importPicker } from './import-picker.js'; import { localModulesUI } from './local-modules-ui.js'; import { tokenize, paintSyntax } from './syntax.js'; -import { backend, backends, runtimeStats, type Backend } from './runtime-backends.js'; +import { backends, runtimeStats } from './runtime-backends.js'; import { setupInspector } from './inspector.js'; import { ActiveClock } from './clock.js'; import { PACKAGE_LOAD_MS, operationFailure, operationTimeout, type Operation } from './execution.js'; @@ -21,10 +21,10 @@ function el<T extends HTMLElement = HTMLElement>(id: string): T { const sourceEditor = new SourceEditor(el('source-editor')); const compileButton = el<HTMLButtonElement>('compile'), restoreButton = el<HTMLButtonElement>('restore-source'); const downloadButton = el<HTMLButtonElement>('download-bundle'), exportModuleButton = el<HTMLButtonElement>('export-compiled-module'); -const backendPicker = el<HTMLSelectElement>('runtime-backend'), programPicker = el<HTMLSelectElement>('program-picker'); +const programPicker = el<HTMLSelectElement>('program-picker'); const versionPicker = el<HTMLSelectElement>('package-version'), hashInput = el<HTMLInputElement>('bundle-hash'); const stopButton = el<HTMLButtonElement>('stop'), shareButton = el<HTMLButtonElement>('share'); -let selectedBackend: Backend = backend(backendPicker.value); +const selectedBackend = 'haskell-wasm'; const workerURL = () => new URL(backends[selectedBackend].worker, import.meta.url); let draft = false, scratch = false, compiled = false, busy = false, loaded = false; let originalSource: string | undefined, compiledSource: string | undefined, compiledRevision: number | undefined; @@ -71,7 +71,7 @@ const definitions = definitionInspector(sourceEditor, (id, name, output) => { pending = undefined; deadline.start(); }, }); -const imports = importPicker({ source: sourceEditor, backend: () => backendPicker.value, +const imports = importPicker({ source: sourceEditor, backend: () => selectedBackend, available: () => !busy, editable: () => !busy && !sourceEditor.dom.hidden && !sourceEditor.isReadOnly, tryLibrary: (library, definition) => { preferredExport = definition; loadBundle(library); }, local: () => localModules.entries(), inputs: () => localModules.inputs() }); @@ -79,7 +79,7 @@ function diagnostic(message = ''): void { el('operation-diagnostic').textContent function ready(): boolean { return !!worker && (draft ? compiled && sourceEditor.getSource() === compiledSource && editRevision === compiledRevision : loaded); } function controls(working: boolean): void { busy = working; localModules.disable(busy); - backendPicker.disabled = busy; programPicker.disabled = busy || !entries.length; + programPicker.disabled = busy || !entries.length; versionPicker.disabled = hashInput.disabled = busy; el<HTMLButtonElement>('load-release').disabled = busy || !programPackage(programPicker.value); el<HTMLButtonElement>('load').disabled = busy; el<HTMLButtonElement>('add-import').disabled = busy; @@ -88,7 +88,6 @@ function controls(working: boolean): void { exportModuleButton.disabled = busy || !draft || !ready(); downloadButton.disabled = busy || draft || downloadHash() === undefined; sourceEditor.setAvailability(busy && operation !== 'compiling', originalSource === undefined && !draft); const editable = !sourceEditor.dom.hidden && (draft || originalSource !== undefined); - el('playground-intro').hidden = selected?.id !== 'names-values' || !editable; el('source-title').textContent = editable ? 'Source' : sourceAvailable ? 'Read-only source' : loaded ? 'Compiled package' : 'Package information'; el('source-availability').hidden = editable || !loaded; el('source-availability').textContent = 'Browse stored definitions below, or import this module into your own source. Inspection does not execute IO.'; @@ -399,7 +398,6 @@ function compileDraft(): void { }catch(error){finish(String(error),true);} } exportModuleButton.onclick=()=>{if(busy||!draft||!ready()||!worker)return;operation='exporting';controls(true);deadline.reset(Infinity);deadline.start();el('status').textContent='Exporting module';el('compiled-module-status').textContent='Observing public trees; Stop is available…';worker.postMessage({type:'export-module'});}; -backendPicker.onchange=()=>{selectedBackend=backend(backendPicker.value);editRevision++;definitions.reset();dispose();if(draft){el('status').textContent='Changes pending';queueCompile(0);}else if(selection)newWorker(true);}; const examples={ identity:'id = (x : x)\nanswer = id 42', contracts:'id x =@nat? x\nanswer = id 42', diff --git a/ext/demo-site/style.css b/ext/demo-site/style.css @@ -2,14 +2,14 @@ :root{color-scheme:light;--paper:#f5f2ea;--ink:#202d29;--muted:#626d65;--green:#235a43;--line:#d8dcd0;--panel:#fffef9;--code:#172a24;--toolbar:#eaf0e6;--dock:#eef2e8;--output:#f3f4ee;--error:#9b3422;--warning:#8b581b;--focus:#b65b28;--primary:var(--green);--on-primary:white;font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif} *{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:var(--paper);color:var(--ink)}a{color:var(--green);text-underline-offset:4px}button,input,select{font:inherit}button,.button{border:1px solid var(--line);background:var(--panel);color:var(--ink);border-radius:6px;padding:10px 14px;cursor:pointer;text-decoration:none;display:inline-block;min-height:44px}button:hover:not(:disabled),.button:hover{border-color:var(--green)}button:disabled{cursor:default;opacity:.45}button.primary,.button.primary{background:var(--primary);color:var(--on-primary);border-color:var(--green)}:focus-visible{outline:3px solid var(--focus);outline-offset:3px}.wrap{max-width:1120px;margin:auto;padding-inline:28px}header{display:flex;justify-content:space-between;align-items:center;padding-block:28px;border-bottom:1px solid var(--line);gap:20px}.wordmark{font-weight:800;font-size:25px;text-decoration:none}.wordmark span{font-size:13px;font-weight:400;color:var(--muted)}nav{display:flex;gap:22px;font-size:14px}.hero{padding-block:64px;position:relative}.eyebrow{font:600 12px ui-monospace,monospace;letter-spacing:.12em;text-transform:uppercase;color:var(--green)}.pronunciation{text-transform:none;letter-spacing:normal}h1{font-size:clamp(48px,6vw,78px);line-height:1.03;letter-spacing:-.055em;font-weight:650;margin:22px 0}h1 span{color:var(--green)}.intro{max-width:570px;font-size:19px;line-height:1.7;color:var(--muted)}.shapes{display:flex;gap:35px;margin:28px 0 35px}.shapes div{width:100px;text-align:center}.shapes svg{width:100px;height:80px;display:block;fill:var(--green)}.shapes>div:first-child circle{transform:translateY(18.5px)}.shapes path{stroke:var(--green);stroke-width:2;fill:none}.shapes strong,.shapes code{display:block;font-size:13px;margin-top:5px}.shapes code{color:var(--muted)}h2{font-size:36px;letter-spacing:-.035em;font-weight:600;margin:8px 0 16px}h3{font-size:15px;font-weight:650;margin:0 0 12px}.hint{font-size:12px;line-height:1.7;color:var(--muted);overflow-wrap:anywhere}label{display:block;font-size:12px;font-weight:600;margin-bottom:6px}select,textarea,input:not([type=checkbox]){font:inherit;background:var(--panel);color:var(--ink);border:1px solid var(--line);border-radius:4px;padding:9px;width:100%;min-width:0}select{min-height:44px}textarea{resize:vertical;font:14px/1.6 ui-monospace,monospace}pre{font:14px/1.65 ui-monospace,monospace;white-space:pre-wrap;overflow-wrap:anywhere}details{margin-top:14px;font-size:13px}summary{cursor:pointer;color:var(--green);min-height:32px}button,input,select,textarea{touch-action:manipulation}[hidden]{display:none!important}.sr-only{position:absolute;width:1px;height:1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap}.error{color:var(--error)}.input-row{display:flex;gap:8px;margin-bottom:12px}.input-row button{white-space:nowrap}.check{display:flex;align-items:center;gap:8px;font-weight:400;margin-block:14px}.check input{flex:none}.demo-picker{display:flex;flex-wrap:wrap;gap:8px}.demo-picker button{font-size:12px} -.workspace{max-width:1064px;margin:0 auto;border:1px solid var(--line);border-radius:12px;background:var(--panel);overflow:clip;scroll-margin-top:16px}.editor-toolbar{position:sticky;top:0;z-index:5;display:flex;align-items:end;gap:12px;flex-wrap:wrap;padding:16px 20px;border-bottom:1px solid var(--line);background:var(--toolbar)}.toolbar-selection{flex:1 1 220px;min-width:0}.toolbar-runtime{flex:0 1 150px;min-width:115px}.toolbar-actions{display:flex;gap:8px;align-items:center;flex-wrap:wrap;max-width:100%}.editor-toolbar #status{flex-basis:100%;font-size:12px;color:var(--muted);min-height:18px}.editor-body{display:grid;grid-template-columns:minmax(0,1fr)}.editor-body.settings-open{grid-template-columns:minmax(0,1fr) 320px}.editor-main{min-width:0}.source-panel{background:var(--code);color:#e3edde;min-width:0}.source-heading{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:16px 20px 8px}.source-heading h2{font-size:14px;letter-spacing:0;margin:0}.source-heading span{font:11px ui-monospace,monospace;color:#b1c1b7}#authorship{font-size:12px;color:#b1c1b7;margin:0;padding:0 20px 12px}#authorship.modified{color:#ffda98}#source-editor{display:block;width:100%;height:460px;min-height:240px;max-height:none;border:0;border-radius:0;padding:18px 20px;margin:0;color:inherit;background:transparent;font:14px/24px ui-monospace,monospace;tab-size:2}#source-editor.modified{background:#273c32;box-shadow:inset 4px 0 #e5b96b}#source-editor:disabled{opacity:.75}#source-notes{margin:0;padding:18px 20px;max-height:460px;overflow:auto;white-space:pre;tab-size:2}#source-availability,#compile-diagnostic,#compile-activity{margin:0;padding:10px 20px;color:#e2d5af}#compile-diagnostic{color:#ffc5b9}.source-actions{display:flex;gap:12px;align-items:center;flex-wrap:wrap;padding:12px 20px;border-top:1px solid #385045}.source-actions .hint{color:#b1c1b7;margin:0}.source-actions button{font-size:12px;padding:6px 10px;background:transparent;color:#e3edde;border-color:#385045} +.workspace{max-width:1064px;margin:0 auto;border:1px solid var(--line);border-radius:12px;background:var(--panel);overflow:clip;scroll-margin-top:16px}.editor-toolbar{position:sticky;top:0;z-index:5;display:flex;align-items:end;gap:12px;flex-wrap:wrap;padding:16px 20px;border-bottom:1px solid var(--line);background:var(--toolbar)}.toolbar-selection{flex:1 1 220px;min-width:0}.toolbar-actions{display:flex;gap:8px;align-items:center;flex-wrap:wrap;max-width:100%}.editor-toolbar #status{flex-basis:100%;font-size:12px;color:var(--muted);min-height:18px}.editor-body{display:grid;grid-template-columns:minmax(0,1fr)}.editor-body.settings-open{grid-template-columns:minmax(0,1fr) 320px}.editor-main{min-width:0}.source-panel{background:var(--code);color:#e3edde;min-width:0}.source-heading{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:16px 20px 8px}.source-heading h2{font-size:14px;letter-spacing:0;margin:0}.source-heading span{font:11px ui-monospace,monospace;color:#b1c1b7}#authorship{font-size:12px;color:#b1c1b7;margin:0;padding:0 20px 12px}#authorship.modified{color:#ffda98}#source-editor{display:block;width:100%;height:460px;min-height:240px;max-height:none;border:0;border-radius:0;padding:18px 20px;margin:0;color:inherit;background:transparent;font:14px/24px ui-monospace,monospace;tab-size:2}#source-editor.modified{background:#273c32;box-shadow:inset 4px 0 #e5b96b}#source-editor:disabled{opacity:.75}#source-notes{margin:0;padding:18px 20px;max-height:460px;overflow:auto;white-space:pre;tab-size:2}#source-availability,#compile-diagnostic,#compile-activity{margin:0;padding:10px 20px;color:#e2d5af}#compile-diagnostic{color:#ffc5b9}.source-actions{display:flex;gap:12px;align-items:center;flex-wrap:wrap;padding:12px 20px;border-top:1px solid #385045}.source-actions .hint{color:#b1c1b7;margin:0}.source-actions button{font-size:12px;padding:6px 10px;background:transparent;color:#e3edde;border-color:#385045} #settings-panel{min-width:0;padding:18px;background:var(--paper);border-left:1px solid var(--line);font-size:13px}#settings-panel section{padding-block:20px;border-bottom:1px solid var(--line)}#settings-panel section:last-child{border:0}.inspector-heading{display:flex;align-items:center;justify-content:space-between;gap:8px}.inspector-heading h2{font-size:16px;margin:0;letter-spacing:0}#settings-panel button{font-size:12px}#settings-panel .hint{margin-bottom:0}#settings-panel details>p{margin-top:8px}#package-info,#release-info,#compiler-stats{overflow-wrap:anywhere}#download-bundle{margin-top:12px;width:100%}#bundle-download-status:empty{display:none}#trace{max-height:260px;overflow:auto;list-style:none;padding:0;font:12px/1.6 ui-monospace,monospace}#trace li{padding-block:8px;border-bottom:1px solid var(--line);overflow-wrap:anywhere;white-space:pre-wrap}#trace li.pending{color:var(--green)}.trace-panel{border-top:1px solid var(--line);padding-top:12px} .explanation{padding-block:64px}.explanation-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:36px;margin-top:28px}.explanation-grid h3{font-size:17px}.explanation-grid p{font-size:14px;line-height:1.8;color:var(--muted)}footer{padding-block:25px 35px;border-top:1px solid var(--line);font-size:12px;line-height:1.8;color:var(--muted)} @media(min-width:950px){.shapes{position:absolute;right:38px;top:70px;flex-direction:column;gap:15px}} @media(max-width:1119px) and (min-width:741px){.workspace{margin-inline:28px}} @media(max-width:949px){.shapes{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px;max-width:570px}.shapes div{width:auto}.shapes svg{max-width:100%;margin-inline:auto}.hero>.button{display:block;width:fit-content;max-width:100%;margin-inline:auto;text-align:center}} -@media(max-width:740px){.wrap{padding-inline:18px}.hero{padding-block:38px}.wordmark span{display:none}header{flex-wrap:wrap;gap:10px;padding-block:20px}nav{gap:14px}nav a{display:inline-flex;align-items:center;min-height:44px}.workspace{width:100%;max-width:none;margin:0;border-left:0;border-right:0;border-radius:0;overflow:visible}.editor-body.settings-open{grid-template-columns:minmax(0,1fr)}.editor-toolbar{padding:14px 16px;gap:10px}.toolbar-selection{flex:1 1 calc(100% - 140px)}.toolbar-runtime{flex:0 0 120px}.toolbar-actions{flex-basis:auto;order:4;display:flex;margin-left:auto}.toolbar-actions button{min-width:0}.source-heading{padding-inline:16px}#authorship{padding-inline:16px}#source-editor{font-size:16px;line-height:27px;padding:16px;width:100%;height:360px;min-height:240px;max-height:none}.source-actions{padding-inline:16px}input,textarea,select{font-size:16px}#settings-panel{position:fixed;inset:12vh 0 0;z-index:20;border:0;border-top:1px solid var(--line);border-radius:16px 16px 0 0;overflow:auto;padding:18px 18px max(24px,env(safe-area-inset-bottom));box-shadow:0 -8px 40px #0003}#settings-backdrop{position:fixed;inset:0;background:#14271e77;z-index:19}.inspector-modal{overflow:hidden}.inspector-heading{position:sticky;top:-18px;background:var(--paper);padding-block:12px;z-index:1}.explanation-grid{grid-template-columns:1fr;gap:16px}.explanation{padding-block:45px}} +@media(max-width:740px){.wrap{padding-inline:18px}.hero{padding-block:38px}.wordmark span{display:none}header{flex-wrap:wrap;gap:10px;padding-block:20px}nav{gap:14px}nav a{display:inline-flex;align-items:center;min-height:44px}.workspace{width:100%;max-width:none;margin:0;border-left:0;border-right:0;border-radius:0;overflow:visible}.editor-body.settings-open{grid-template-columns:minmax(0,1fr)}.editor-toolbar{padding:14px 16px;gap:10px}.toolbar-selection{flex:1 1 220px}.toolbar-actions{flex-basis:auto;order:4;display:flex;margin-left:auto}.toolbar-actions button{min-width:0}.source-heading{padding-inline:16px}#authorship{padding-inline:16px}#source-editor{font-size:16px;line-height:27px;padding:16px;width:100%;height:360px;min-height:240px;max-height:none}.source-actions{padding-inline:16px}input,textarea,select{font-size:16px}#settings-panel{position:fixed;inset:12vh 0 0;z-index:20;border:0;border-top:1px solid var(--line);border-radius:16px 16px 0 0;overflow:auto;padding:18px 18px max(24px,env(safe-area-inset-bottom));box-shadow:0 -8px 40px #0003}#settings-backdrop{position:fixed;inset:0;background:#14271e77;z-index:19}.inspector-modal{overflow:hidden}.inspector-heading{position:sticky;top:-18px;background:var(--paper);padding-block:12px;z-index:1}.explanation-grid{grid-template-columns:1fr;gap:16px}.explanation{padding-block:45px}} @media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}} @@ -58,7 +58,6 @@ header nav{align-items:center;gap:12px}#theme-separator{color:var(--muted)} .definition-card button:hover{color:#e3edde;background:#385045} .definition-card .hint{color:#b1c1b7;font-size:11px;margin:6px 0 0} .definition-card pre{max-height:160px;overflow:auto;white-space:pre-wrap;overflow-wrap:anywhere;margin:0;padding:2px 0 6px;font-size:18px;line-height:1.5} -#playground-intro{margin:0;padding:8px 20px 14px;color:#d2e0ce;font-size:13px}#playground-intro a{color:inherit;text-decoration:underline} #import-exports{display:flex;gap:6px;flex-wrap:wrap}#import-exports button{font:inherit;font-size:13px;padding:5px 8px} #import-results h3{margin-bottom:0}#try-library,#copy-qualified{margin:8px 8px 8px 0} diff --git a/ext/demo-site/test/asset-cache-probe.mjs b/ext/demo-site/test/asset-cache-probe.mjs @@ -7,7 +7,6 @@ const change=(id,value)=>{el(id).value=value;el(id).dispatchEvent(new Event('cha const input=(id,value)=>{if(id==='source-editor'){globalThis.__sourceEditor.replaceSource(value,true);return;}el(id).value=value;el(id).dispatchEvent(new Event('input'));}; export async function run(control,backend='haskell-wasm'){ await status('Ready'); - if(el('runtime-backend').value!==backend){change('runtime-backend',backend);await status('Ready');} const base=new URL('../',document.querySelector('script[type=module][src]').src); assert(/\/assets\/[a-f0-9]{64}\/$/.test(base.pathname),'Testing a versioned release'); const html=await fetch(new URL('index.html?noProbe=1',location.href)); diff --git a/ext/demo-site/test/backend-probe.mjs b/ext/demo-site/test/backend-probe.mjs @@ -5,10 +5,10 @@ export async function run(control,backend='haskell-wasm'){ const requests=async()=>await(await fetch(control+'/requests')).json(); const fault=async values=>{const response=await fetch(control+'/fault',{method:'POST',body:JSON.stringify(values)});assert(response.ok,'Fault control');}; const select=async name=>{change('program-picker','demo:'+name);await status('Ready');}; - assert(el('runtime-backend').value==='haskell-wasm','Fresh page defaults to Haskell'); + assert(!el('runtime-backend'),'Runtime is fixed without a selector'); assert(await inspect('greeting')==='Hello, world!','Default real Haskell inspection'); assert((await requests()).some(path=>path.endsWith('/runtime/haskell/reactor.wasm')),'Haskell is really loaded'); - await fault({runtime:true});change('runtime-backend',backend);await status('Stopped'); + await fault({runtime:true});change('program-picker','demo:arithmetic');await status('Stopped'); assert(/WASM fetch failed|HTTP|fetch/i.test(el('operation-diagnostic').textContent),'Runtime artifact failure is visible, no JS fallback'); change('program-picker','demo:arithmetic');await status('Stopped'); await fault({runtime:false});el('reload-package').click();await status('Ready'); @@ -34,7 +34,7 @@ export async function run(control,backend='haskell-wasm'){ document.querySelector('[data-compiler-example=identity]').click();input('source-editor','answer = 42\nloop = (x : x x) (x : x x)');await status('Compiled'); document.querySelector('[aria-label="Inspect compiled definition loop"]').click();await wait(()=>el('status').textContent==='Inspecting loop','CPU-bound observation');await pause(100);el('stop').click(); assert(!el('compile').disabled,'Stop discards runtime and permits recompile'); - const source=globalThis.__sourceEditor.getSource();change('runtime-backend',backend);await status('Compiled');assert(globalThis.__sourceEditor.getSource()===source,'Runtime restart preserves source'); + const source=globalThis.__sourceEditor.getSource();el('compile').click();await status('Compiled');assert(globalThis.__sourceEditor.getSource()===source,'Runtime restart preserves source'); assert(await inspect('answer','number')==='42','Runtime restart rebuilds independent definitions'); await(await import('/backend-resume-probe.mjs')).probeResume(backend); return {...report,checks:[...report.checks,'real Haskell default; package/practical compilation; native statistics; decoder recovery; artifact/pin failures without fallback; CPU-bound Stop; backend isolation; stale/malformed resume rejection']}; diff --git a/ext/demo-site/test/basic-examples-probe.mjs b/ext/demo-site/test/basic-examples-probe.mjs @@ -13,25 +13,27 @@ export async function run(control,backend='haskell-wasm'){ assert(groups[2].firstElementChild.value==='demo:language-reference','Reference leads explorations'); assert(![...el('program-picker').options].some(o=>o.value==='demo:intro'||o.value==='package:intro'),'Start Here retired from selector'); assert(el('program-picker').value==='demo:names-values'&&!el('expression-editor'),'Beginner arrival is source-first'); - assert(document.querySelector('.definition-card').hidden&&el('terminal').textContent==='','Arrival waits for explicit inspection'); - change('runtime-backend',backend);await status('Ready'); + assert(document.querySelector('.definition-card').hidden,'Arrival waits for explicit inspection'); const base=new URL('../',document.querySelector('script[type=module][src]').src); const releases=(await(await fetch(new URL('packages.json',base))).json()).releases; assert(![...el('program-picker').options].some(o=>o.value.startsWith('package:')),'Libraries are discovered in the explorer, not a giant selector list'); - assert(!el('playground-intro').hidden&&el('playground-intro').textContent.includes('definition name'),'First lesson explains inspection and evaluation'); assert(el('add-import').textContent==='Explore libraries…','Prominent library explorer'); const select=async name=>{change('program-picker','demo:'+name);await status('Ready');}; const evaluate=async(expression,output,expected)=>assert(await evaluateSource(expression,output)===expected,expression+' expected '+expected); - const defaults=[['greeting','string','Hello, world!'],['total','number','12'],['double 6','number','12'],['count','number','3'],['total','number','24'],['message 0','string','Your basket is empty.']]; + const defaults=[['greeting','string','Hello, world!'],['total','number','12'],['sixDoubled','number','12'],['itemCount','number','3'],['total','number','24'],['emptyBasket','string','Your basket is empty.']]; for(let i=0;i<basics.length;i++){ await select(basics[i]);const original=globalThis.__sourceEditor.getSource(); - await evaluate(...defaults[i]); + assert(await inspect(...defaults[i].slice(0,2))===defaults[i][2],'Published lesson result '+basics[i]); + if(i===0)assert(await inspect('greetAndAnswer','values')==='["Hello, world!" 42]','Intro list is published and inspectable'); + if(i===2){assert(await inspect('nineDoubled','number')==='18','Second function call');assert(await inspect('area','number')==='28','Two-parameter function');} + if(i===4){assert(await inspect('doubled','values')==='[4 8 12]','Mapped list');assert(await inspect('numbers','values')==='[2 4 6]','Original list preserved');} + if(i===5){assert(await inspect('oneItem','string')==='You have one item.','Specific match');assert(await inspect('fullBasket','string')==='You have several items.','Fallback match');} if(!el('restore-source').hidden){el('restore-source').click();await status('Ready');} if(i===0){input('source-editor',original.replace('answer = 42','answer = 7'));await status('Compiled');await evaluate('answer','number','7');} - if(i===1){input('source-editor',original.replace('Math.add 5 7','Math.add 8 9'));await status('Compiled');await evaluate('total','number','17');} - if(i===2){input('source-editor',original.replace('double number = Math.multiply number 2','triple number = Math.multiply number 3'));await status('Compiled');await evaluate('triple 6','number','18');} - if(i===3){input('source-editor',original.replace('numbers = [2 4 6]','numbers = [2 4 6 8]'));await status('Compiled');await evaluate('count','number','4');await evaluate('total','number','20');input('source-editor',original.replace('numbers = [2 4 6]','numbers = []'));await status('Compiled');await evaluate('count','number','0');await evaluate('total','number','0');} - if(i===4){input('source-editor',original.replace('Math.multiply number 2','Math.multiply number 3'));await status('Compiled');await evaluate('total','number','36');} + if(i===1){input('source-editor',original.replace('add 5 7','add 8 9'));await status('Compiled');await evaluate('total','number','17');} + if(i===2){input('source-editor',original.replace('mul number 2','mul number 3').replace(/\bdouble\b/g,'triple'));await status('Compiled');await evaluate('sixDoubled','number','18');await evaluate('nineDoubled','number','27');} + if(i===3){input('source-editor',original.replace('numbers = [2 4 6]','numbers = [2 4 6 8]'));await status('Compiled');await evaluate('itemCount','number','4');await evaluate('total','number','20');input('source-editor',original.replace('numbers = [2 4 6]','numbers = []'));await status('Compiled');await evaluate('itemCount','number','0');await evaluate('total','number','0');} + if(i===4){input('source-editor',original.replace('mul number 2','mul number 3'));await status('Compiled');await evaluate('total','number','36');} if(i===5){input('source-editor',original.replace('message _ =','message 2 = "You have two items."\nmessage _ ='));await status('Compiled');await evaluate('message 2','string','You have two items.');await evaluate('message 4','string','You have several items.');} el('restore-source').click();await status('Ready');assert(globalThis.__sourceEditor.getSource()===original,'Restore '+basics[i]); } diff --git a/ext/demo-site/test/definition-names-probe.mjs b/ext/demo-site/test/definition-names-probe.mjs @@ -3,7 +3,6 @@ import {el,input,change,status,wait,inspect,card,result,pause,sourceEditor} from const assert=(ok,message)=>{if(!ok)throw Error(message);}; export async function run(control,backend='haskell-wasm'){ await status('Ready'); - if(el('runtime-backend').value!==backend){change('runtime-backend',backend);await status('Ready');} document.querySelector('[data-compiler-example=identity]').click(); const long='aVeryLongDefinitionName'+ 'Extended'.repeat(9); const source=`-- λ🙂 positions stay native UTF-16 offsets diff --git a/ext/demo-site/test/firefox-idle.mjs b/ext/demo-site/test/firefox-idle.mjs @@ -36,7 +36,7 @@ async function evaluate(context,expression){ const snapshot=context=>evaluate(context,`(()=>{ const get=id=>document.getElementById(id); return {at:new Date().toISOString(),visibility:document.visibilityState,status:get('status')?.textContent, - backend:get('runtime-backend')?.value,program:get('program-picker')?.value, + backend:'haskell-wasm',program:get('program-picker')?.value, sourceHidden:get('source-editor')?.hidden,sourceReadOnly:globalThis.__sourceEditor?.isReadOnly, sourceDisabled:globalThis.__sourceEditor?.isDisabled,source:globalThis.__sourceEditor?.getSource(), result:document.querySelector('.definition-card pre')?.textContent, diff --git a/ext/demo-site/test/ide-probe.mjs b/ext/demo-site/test/ide-probe.mjs @@ -4,11 +4,10 @@ const assert=(condition,message)=>{if(!condition)throw Error(message);}; export async function run(control,backend='haskell-wasm'){ const phase=name=>fetch(control+'/phase',{method:'POST',body:JSON.stringify({name:'ide: '+name})}); await status('Ready');await syntaxProbe();await status('Ready'); - assert([...el('runtime-backend').options].map(o=>o.value).join(',')==='haskell-wasm','Only production runtimes'); + assert(!el('runtime-backend'),'No runtime selector'); for(const path of ['worker.js','compiler-worker.js','conformance/worker.js','conformance/compiler-worker.js'])assert((await fetch(new URL('./'+path,document.querySelector('script[type=module][src]').src))).status===404,'JS conformance Workers not shipped'); assert(!el('expression-editor')&&card().hidden,'Arrival is source-first and does not inspect automatically'); el('settings-toggle').click();assert(!el('settings-panel').hidden,'Settings opens');el('settings-close').click();assert(document.activeElement===el('settings-toggle'),'Settings returns focus'); - if(el('runtime-backend').value!==backend){change('runtime-backend',backend);await status('Ready');} change('program-picker','demo:language-reference');await status('Ready'); for(const [format,want] of [['tricu','t'],['triangle','△'],['dag','tree △\ntree\n'],['ternary','0']])assert(await inspect('leaf',format)===want,'Direct definition '+format); for(const [format,want] of [['tricu','(t t t)'],['triangle','(△ △ △)'],['ternary','200']])assert(await evaluateSource('identity (t t t)',format)===want,'Source-authored application '+format); diff --git a/ext/demo-site/test/import-folds-probe.mjs b/ext/demo-site/test/import-folds-probe.mjs @@ -2,7 +2,7 @@ import {sourceEditor as editor,input,change,status,el,inspect,pause} from './source-probe-helpers.mjs'; const assert=(ok,message)=>{if(!ok)throw Error(message);}; export async function run(_control,backend='haskell-wasm'){ - await status('Ready');if(el('runtime-backend').value!==backend){change('runtime-backend',backend);await status('Ready');} + await status('Ready'); const catalog=await(await fetch(new URL('../reference/libraries.json',document.querySelector('script[type=module][src]').src))).json(); const releases=catalog.releases; const hash=name=>releases.find(row=>row.name==='tricu-lib-'+name&&row.version==='0.3.0').manifest; diff --git a/ext/demo-site/test/import-picker-probe.mjs b/ext/demo-site/test/import-picker-probe.mjs @@ -78,8 +78,7 @@ outside = zeros 4097`; assert(rejected,'Malformed header rejected'); assert((await session.runExpression('answer','number','pure',host)).value==='42','Failed inspection preserves compiled scope'); }finally{session.dispose();} - await status('Ready');change('runtime-backend',backend);await status('Ready'); - assert(!el('playground-intro').hidden,'Brief Playground tutorial on arrival'); + await status('Ready'); const inspectButton=name=>document.querySelector(`[aria-label="Inspect compiled definition ${name}"]`); inspectButton('greeting').click();await wait(()=>!document.querySelector('.definition-card select').disabled,'published inspection'); const interpretation=value=>{const select=document.querySelector('.definition-card select');select.value=value;select.dispatchEvent(new Event('change'));}; diff --git a/ext/demo-site/test/inline-io-probe.mjs b/ext/demo-site/test/inline-io-probe.mjs @@ -8,7 +8,6 @@ async function wait(check,label){for(let i=0;i<3600;i++){if(check())return;await const status=text=>wait(()=>el('status').textContent===text,text); export async function run(control,backend='haskell-wasm'){ await status('Ready'); - if(el('runtime-backend').value!==backend){change('runtime-backend',backend);await status('Ready');} assert(!el('expression-editor')&&!el('run')&&!document.querySelector('.expression-dock'),'No separate evaluation component'); const base=new URL('../',document.querySelector('script[type=module][src]').src); const api=await import(new URL('dist/probe-api.js',base)); diff --git a/ext/demo-site/test/io-public-probe.mjs b/ext/demo-site/test/io-public-probe.mjs @@ -62,7 +62,7 @@ export async function run(_control,backend){ }finally{doomed.dispose();} checks.push('emergency runtime disposal rejects idle read and explicitly does not acknowledge cleanup'); // Real mounted editor + outer worker + nested native worker, not a mock host. - await status('Ready');if(el('runtime-backend').value!==backend){change('runtime-backend',backend);await status('Ready');} + await status('Ready'); const index=await(await fetch(new URL('packages.json',base))).json(); const io=index.releases.find(r=>r.title==='lib/io.tri'&&r.version==='0.4.0');assert(io,'authenticated complete library'); const source=`!import ${io.manifest} as IO\n!export answer\n!export binary\n!export waiting\n!export caps\n!export classification\n!export divergentValue\ndivergentValue = IO.io (IO.pure ((x : x x) (x : x x)))\nbackendOf rows = case rows of\n [["backend" value] | _] = value\n [_ | rest] = backendOf rest\n _ = "missing"\ncaps = IO.io (IO.require IO.capabilities (rows : IO.pure (backendOf rows)))\nloop = (x : x x) (x : x x)\nclassification = IO.io (IO.require (IO.inspect (IO.request "file" 1 "open" loop)) IO.pure)\nanswer = 42\nbinary = IO.io (do IO.require\n writer <- IO.stdout\n IO.writeAll writer [92 120 102 102 255 240 159]\n IO.pure "returned text")\nwaiting = IO.io (do IO.require\n reader <- IO.stdin\n IO.readSome reader 4\n IO.pure 42)`; diff --git a/ext/demo-site/test/live-ux-probe.mjs b/ext/demo-site/test/live-ux-probe.mjs @@ -1,7 +1,7 @@ import {el,input,change,status,wait,pause,inspect,result,card} from './source-probe-helpers.mjs'; const assert=(condition,message)=>{if(!condition)throw Error(message);}; export async function run(control,backend='haskell-wasm'){ - await status('Ready');if(el('runtime-backend').value!==backend){change('runtime-backend',backend);await status('Ready');} + await status('Ready'); change('program-picker','demo:language-reference');await status('Ready');assert(await inspect('answer','number')==='42','Initial published value'); const NativeWorker=globalThis.Worker,held=[],submitted=[];let hold=true; // Delay real native replies to make late-completion ordering deterministic. @@ -22,8 +22,8 @@ export async function run(control,backend='haskell-wasm'){ hold=false;held.shift()();await status('Compiled');assert(el('compile-diagnostic').hidden,'Stale diagnostics discarded'); input('source-editor','answer = missingLatestName');await status('Compilation failed');assert(!el('compile-diagnostic').hidden&&card().querySelector('select').disabled&&result()==='','Latest diagnostic invalidates old inspection'); input('source-editor','identity value = value\nanswer =\n identity\n 46');await status('Compiled');assert(await inspect('answer','number')==='46','Indented continuation'); - change('runtime-backend',backend);await status('Compiled');assert(await inspect('answer','number')==='46','Runtime restart rebuilds source'); - change('runtime-backend',backend);await status('Compiled'); + el('stop').click();el('compile').click();await status('Compiled');assert(await inspect('answer','number')==='46','Runtime restart rebuilds source'); + el('stop').click();el('compile').click();await status('Compiled'); input('source-editor','answer = 47');el('stop').click();await pause(900);assert(el('status').textContent==='Stopped','Stop suppresses queued compilation'); input('source-editor','answer = 48');await status('Compiled');assert(await inspect('answer','number')==='48','Source edits recover after Stop'); el('source-editor').dispatchEvent(new CompositionEvent('compositionstart'));input('source-editor','answer = 49');await pause(900);assert(el('status').textContent==='Changes pending','IME does not compile mid-composition'); diff --git a/ext/demo-site/test/local-module-probe.mjs b/ext/demo-site/test/local-module-probe.mjs @@ -51,7 +51,7 @@ export async function run(control,backend){ await rejects(()=>s.run('main',[],'string','io',host),/Unsupported|unsupported/); }finally{s.dispose();} - await status('Ready');change('runtime-backend',backend);await status('Ready'); + await status('Ready'); document.querySelector('[data-compiler-example=module]').click();await status('Compiled'); input('source-editor',source);el('compile').click();await status('Compiled'); const createURL=URL.createObjectURL,click=HTMLAnchorElement.prototype.click;let downloaded,name; @@ -81,7 +81,7 @@ export async function run(control,backend){ el('export-compiled-module').click();await status('Exporting module');await delay(150);el('stop').click();await status('Stopped'); assert(el('export-compiled-module').disabled,'Stop invalidates exportable draft'); input('source-editor',consumer);el('compile').click();await status('Compiled'); - change('runtime-backend',backend); + el('stop').click(); el('compile').click();await status('Compiled');equal(await evaluateSource('answer'),'42','Local bytes survive Stop and runtime replacement'); const frame=document.createElement('iframe');frame.src=new URL('index.html?noProbe=1',location.href).href;document.body.append(frame); await wait(()=>frame.contentDocument?.getElementById('status')?.textContent==='Ready'); diff --git a/ext/demo-site/test/module-probe.mjs b/ext/demo-site/test/module-probe.mjs @@ -125,7 +125,7 @@ export async function run(control,backend='haskell-wasm'){ }finally{s.dispose();await fault({moduleMissing:null,moduleCorrupt:null,moduleManifest:null});} await phase('page integration'); // Exercise the actual editable page, including Worker termination and rebuilding. - await status('Ready');change('runtime-backend',backend);await status('Ready'); + await status('Ready'); document.querySelector('[data-compiler-example=module]').click();await status('Compiled'); assert(globalThis.__sourceEditor.getSource().includes(math.manifest),'Module example uses an indexed manifest, not a bundle digest'); equal(await inspect('answer','number'),'42','Module scratch example'); diff --git a/ext/demo-site/test/program-catalogue.test.mjs b/ext/demo-site/test/program-catalogue.test.mjs @@ -24,7 +24,39 @@ test('beginner progression and reference are separated from practical demos',asy assert.equal(catalogue.filter(e=>exampleGroup(e)==='Language & Runtime Explorations')[0].id,'language-reference'); assert.equal(catalogue[0].export,'greeting');assert.equal(catalogue[0].output,'string'); for(const entry of catalogue.filter(e=>exampleGroup(e)==='Basic Examples')){ - assert.match(entry.source,/-- Evaluate /);assert.match(entry.source,/-- Exercise:/); + assert.equal(entry.source,await readFile(new URL('../demos/'+entry.id+'.tri',import.meta.url),'utf8')); + assert.match(entry.source,/-- Next:|Choose "Doing arithmetic"/); + if(entry.id!=='names-values'){ + assert.doesNotMatch(entry.source,/Click or tap|compile after|dropdown|inspection card/); + assert.doesNotMatch(entry.source,/^!import .* as /m); + } + assert.doesNotMatch(entry.source,/Evaluate|Choose .* in Definition|Add import supplies/); } assert.equal(Object.keys(catalogue.find(e=>e.id==='language-reference').presets).length,16); }); + +test('example sources stay synchronized and omit retired evaluator controls',async()=>{ + const catalogue=await read('catalogue.json'); + for(const entry of catalogue.filter(e=>['Basic Examples','Practical Examples'].includes(exampleGroup(e)))){ + assert.equal(entry.source,await readFile(new URL('../demos/'+entry.id+'.tri',import.meta.url),'utf8')); + assert.doesNotMatch(entry.source,/Evaluate|their presets|Select v1\/v2|Record fields/); + } + const html=await readFile(new URL('../index.html',import.meta.url),'utf8'); + const app=await readFile(new URL('../dist/app.js',import.meta.url),'utf8'); + assert(!html.includes('playground-intro'));assert(!app.includes('playground-intro')); +}); + +test('basic examples publish results as well as the functions they teach',async()=>{ + const catalogue=await read('catalogue.json'); + const results={ + 'names-values':['greetAndAnswer'], + 'functions':['sixDoubled','nineDoubled','area'], + 'lists':['numbers','itemCount','total'], + 'transform-list':['numbers','doubled','total'], + 'choices':['emptyBasket','oneItem','fullBasket'], + }; + for(const [id,names] of Object.entries(results)){ + const entry=catalogue.find(e=>e.id===id); + for(const name of names)assert.deepEqual(entry.presets[name].arguments,[],id+': '+name); + } +}); diff --git a/ext/demo-site/test/runtime-backends.test.mjs b/ext/demo-site/test/runtime-backends.test.mjs @@ -17,14 +17,12 @@ test('runtime diagnostics do not invent graph counters for WASM',()=>{ assert.doesNotMatch(value,/work|heap nodes|allocated/); }); -test('the real page offers only the Haskell runtime',async()=>{ +test('the page uses Haskell directly without a runtime selector',async()=>{ const html=await readFile(new URL('../index.html',import.meta.url),'utf8'); - const selector=html.match(/<select id="runtime-backend">([\s\S]*?)<\/select>/)[1]; - assert.match(selector,/^<option value="haskell-wasm" selected>Haskell<\/option>/); - assert.doesNotMatch(selector,/JavaScript|value="js"/); - assert.doesNotMatch(selector.replace(/<[^>]*>/g,''),/WASM|reference/i); - assert.equal((selector.match(/\bselected\b/g)||[]).length,1); - assert.equal((selector.match(/<option /g)||[]).length,1); + const app=await readFile(new URL('../src/app.ts',import.meta.url),'utf8'); + assert.doesNotMatch(html,/runtime-backend|toolbar-runtime/); + assert.match(app,/const selectedBackend = 'haskell-wasm'/); + assert.doesNotMatch(app,/backendPicker|['"]runtime-backend['"]/); }); test('production editor has no checkpoint restore or JS Worker branch',async()=>{ diff --git a/ext/demo-site/tutorials.py b/ext/demo-site/tutorials.py @@ -15,16 +15,23 @@ def examples(): # presets; repeated cases exercise fallback paths without changing a preset. return { 'names-values': ('Names and values', 'Basic Examples', [ - ('greeting', [], 'Hello, world!', 'string'), ('answer', [], 42, 'number')]), + ('greeting', [], 'Hello, world!', 'string'), ('answer', [], 42, 'number'), + ('greetAndAnswer', [], ['Hello, world!', 42], 'values')]), 'arithmetic': ('Doing arithmetic', 'Basic Examples', [ - ('total', [], 12, 'number'), ('product', [], 35, 'number'), ('grouped', [], 24, 'number')]), + ('total', [], 12, 'number'), ('multiplied', [], 35, 'number'), ('grouped', [], 24, 'number')]), 'functions': ('Your first function', 'Basic Examples', [ - ('double', [6], 12, 'number'), ('double', [9], 18, 'number'), ('add', [4, 7], 11, 'number')]), + ('sixDoubled', [], 12, 'number'), ('nineDoubled', [], 18, 'number'), + ('double', [6], 12, 'number'), ('double', [9], 18, 'number'), + ('rectangle', [4, 7], 28, 'number'), ('area', [], 28, 'number')]), 'lists': ('Working with lists', 'Basic Examples', [ - ('count', [], 3, 'number'), ('total', [], 12, 'number')]), + ('itemCount', [], 3, 'number'), ('total', [], 12, 'number'), ('numbers', [], [2, 4, 6], 'values')]), 'transform-list': ('Transforming a list', 'Basic Examples', [ - ('total', [], 24, 'number'), ('double', [5], 10, 'number')]), + ('total', [], 24, 'number'), ('double', [5], 10, 'number'), + ('numbers', [], [2, 4, 6], 'values'), ('doubled', [], [4, 8, 12], 'values')]), 'choices': ('Making a choice', 'Basic Examples', [ + ('emptyBasket', [], 'Your basket is empty.', 'string'), + ('oneItem', [], 'You have one item.', 'string'), + ('fullBasket', [], 'You have several items.', 'string'), ('message', [0], 'Your basket is empty.', 'string'), ('message', [1], 'You have one item.', 'string'), ('message', [4], 'You have several items.', 'string')]), @@ -60,7 +67,6 @@ def settings(): first = cases[0] result[name] = dict(title=title, category=category, exports=list(presets), export=first[0], args=presets[first[0]]['arguments'], output=first[3], mode='pure', presets=presets) - # Private list values keep the beginner definition picker on scalar results. return result