commit 53f7aa7d422102a47b2ba2415acd86ada2bbde8f parent 77da59915a8039e77273284ae1f6e6701d8dfbc4 Author: James Eversole <james@eversole.co> Date: Mon, 19 Oct 2026 16:14:12 -0500 Introduce beginner lessons and an editable language reference Make Names and values the default, organize six Basic Examples ahead of practical and language/runtime examples, and retire Start Here from the selector while preserving its immutable releases and runtime fixtures. Deduplicate library rows by native manifest identity, prefer complete library publications, and simplify display labels. Add native-verified lesson expectations and browser coverage for defaults, exercises, restoration, reference presets and historical links. Use whole-pixel editor line heights to keep long-source syntax mirrors aligned. Validated both backends in Chromium, Firefox and Linux WebKit, 129 unit tests, generated-JS parity and nix flake check. Diffstat:
44 files changed, 1381 insertions(+), 98 deletions(-)
diff --git a/ext/demo-site/README.md b/ext/demo-site/README.md @@ -5,7 +5,7 @@ stored definitions in the browser. The site lives in `ext/demo-site` and uses TypeScript compiled to browser ES modules. The page flows from the hero into one editor workspace, followed by the ecosystem -articles. **Start Here** loads on arrival, including when no selection fragment is +articles. **Names and values** loads on arrival, including when no selection fragment is present; loading verifies a package but never executes it or downloads a compiler kit. Haskell is the initial runtime. @@ -182,9 +182,24 @@ All curated and arbitrary packages take the same verified bundle/heap execution path. Curated catalogue entries add suggested literals, codecs, and optional source. -- **Start here** (default): a comment-led language guide with eight runnable - exports covering definitions, lambdas, application, constructor forms, and tree - literals. Each export suggests inputs and an appropriate result interpretation. +The selector starts with **Basic Examples**, in learning order: + +- **Names and values** (default): name a greeting and a number, then change them. +- **Doing arithmetic**: call arithmetic functions and group calculations. +- **Your first function**: name parameters and reuse a calculation. +- **Working with lists**: collect values, count them and find their sum. +- **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 an initial expression, interpretation and expected result, +followed by a small edit exercise. **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, +recursion, `case`, pipes, explicit `do` and contracts. Its sixteen definitions +have runnable presets; module-header alternatives and protocol guides are +explained alongside the examples. + - **Tree microscope**: `shape`, logical `size`, structural `same` on data or program trees. Example identity tree: `21100`, with logical size 5. - **RGB validator**: a small tricu-authored structural guard for exactly three @@ -211,7 +226,14 @@ Conversation IO and repository hosts illustrate effect-protocol adaptation. ### Published libraries -The index also includes `tricu-base`, `tricu-file-io`, and +**Libraries** uses short display labels. Identical native manifest identities +appear once, preferring the complete filesystem-library publication over its +older package alias. Different interfaces retain separate rows, including the +small convenience interfaces and combined compiler tooling. Package names, +versions and both hashes remain in details; all historical releases, including +the retired Start Here example, remain available by bundle hash. + +The retained index also includes `tricu-base`, `tricu-file-io`, and `arboricx-inspection` at `0.1.0` and `0.1.1`. The latter are rebuilt with the current published stable compiler; original releases remain immutable. These are reusable native library releases, not @@ -398,7 +420,7 @@ link. Copy important source elsewhere before leaving. ### Arguments, results, and IO **Evaluate tricu expression** is the default input. Examples include -`identity (first 42 7)` in the intro demo and `append "hello " "world"` in the +`identity (first 42 7)` in Language reference and `append "hello " "world"` in the published base library. Expressions may use literals, applications, lambdas and local bindings. Their environment contains exactly the selected package's exports or current draft scope (local definitions plus explicit imports). Native @@ -439,7 +461,7 @@ for the literal editor. Raw kinds remain string, natural decimal string, Boolean bytes, recursively typed list, and ternary tree. Switching editors preserves valid values; invalid literal fields must be corrected before switching to raw. Partial and multiple applications are ordinary left-folded Tree Calculus application. -The current Start Here guide uses expression input and the current result +The Basic Examples use expression input and the current result interpretations; historical package releases retain their original content. Value interpretations include text, natural number, Boolean, bytes and scalar @@ -1065,19 +1087,28 @@ tests, not as runtime settings. These are graph-cell metrics, not total JavaScript object/heap byte measurements. Large live results can exhaust memory; constructor batches can allocate before another reducer safe point. +The `TEST_BASIC_EXAMPLES=1` browser suite checks the beginner arrival, selector +categories and identity deduplication, all six initial examples and edit exercises, +restoration, every Language reference preset, source editing, and a retained +historical share link. Run it for each backend using the Chromium, Firefox or +Linux WebKit harness against a built site. + ## Application preparation -All ten demo sources in `demos/` are editable native modules. Start Here teaches -constructors, functions, application and literals without imports. Conversation +All sixteen current catalogue sources in `demos/` are editable native modules. +The six Basic Examples teach one concept at a time; Language reference supplies +an editable syntax sampler. Conversation imports IO; RGB imports canonical-byte validation. Tree microscope keeps the structural traversal visible but imports successor/Boolean operations. Business examples import base arithmetic and, where needed, Arboricx's Unicode scalar-text validator. Their small record literals describe the demo output schema directly. Parser and lexer import the complete prelude/parser/lexer interfaces. -`prepare.py` compiles the eight core sources through the authenticated shipped -native compiler, importing their exact dependency bundles into a temporary indexed -store. It checks public interfaces, independent application/IO goldens, malformed +`prepare.py` compiles the core sources and lessons through the authenticated +shipped native compiler, importing their exact dependency bundles into a temporary +indexed store. `tutorials.py` holds lesson settings and independent expectations. +The retired `intro.tri` is still checked as a historical runtime/codec fixture; +it is excluded from the active catalogue. It checks public interfaces, independent application/IO goldens, malformed values and nonforcing demand probes before updating assets. `prepare-index.py` uses `practical-support.tri` to reproduce the standalone test profile. @@ -1303,7 +1334,7 @@ finite reference-harness memory guard, and can fail at that guard on large examp ```sh nix shell --inputs-from . nixpkgs#nodejs -c python3 ext/demo-site/test/run-bounded.py \ - --seconds 600 -- node --max-old-space-size=1024 ext/demo-site/test/editable-conformance.mjs intro + --seconds 600 -- node --max-old-space-size=1024 ext/demo-site/test/editable-conformance.mjs language-reference ``` To genuinely recompile curated packages / Haskell goldens: diff --git a/ext/demo-site/bundles/1972c98bfe599fef2a72cc946401a5cc9280026a8c46fdd7c8ccdc7093ee90cc.arboricx b/ext/demo-site/bundles/1972c98bfe599fef2a72cc946401a5cc9280026a8c46fdd7c8ccdc7093ee90cc.arboricx Binary files differ. diff --git a/ext/demo-site/bundles/38110ab45a75a9c8771d6422277cb3083e667a4368aa33df0b43af672f17375e.arboricx b/ext/demo-site/bundles/38110ab45a75a9c8771d6422277cb3083e667a4368aa33df0b43af672f17375e.arboricx Binary files differ. diff --git a/ext/demo-site/bundles/3cc5d36c5b70a20362117e3349aa044efca8adca56105424965065395a9f3e76.arboricx b/ext/demo-site/bundles/3cc5d36c5b70a20362117e3349aa044efca8adca56105424965065395a9f3e76.arboricx Binary files differ. diff --git a/ext/demo-site/bundles/86717d7f9d69fe730c6c6172c42c49895623aa070bf675e1b5f751adb30616c1.arboricx b/ext/demo-site/bundles/86717d7f9d69fe730c6c6172c42c49895623aa070bf675e1b5f751adb30616c1.arboricx Binary files differ. diff --git a/ext/demo-site/bundles/8d1b7e1f6dfefe983167ee8d9631476caaa0c3dc689a85628e9ca627ebaef6d0.arboricx b/ext/demo-site/bundles/8d1b7e1f6dfefe983167ee8d9631476caaa0c3dc689a85628e9ca627ebaef6d0.arboricx Binary files differ. diff --git a/ext/demo-site/bundles/9f10d772b42628acfe843eec7524d8de1ba9ea4344d3952c6c091c98c4d1c72d.arboricx b/ext/demo-site/bundles/9f10d772b42628acfe843eec7524d8de1ba9ea4344d3952c6c091c98c4d1c72d.arboricx Binary files differ. diff --git a/ext/demo-site/bundles/be5f4675c9b0b6d9c093fe44a9695da9f428c88d62b0f4f1340017067fa81fa9.arboricx b/ext/demo-site/bundles/be5f4675c9b0b6d9c093fe44a9695da9f428c88d62b0f4f1340017067fa81fa9.arboricx Binary files differ. diff --git a/ext/demo-site/catalogue.json b/ext/demo-site/catalogue.json @@ -1,10 +1,195 @@ [ { - "id": "intro", - "title": "Start here", - "hash": "64e7db3b35121efa19c7becbbb38a0ad0f884143e790e286a5ee6c236c00f9d5", - "source": "!export *\n\n-- Start here: a small tricu language guide.\n-- Lines beginning with -- are comments. Read down, then try the exports.\n-- Edit this source in place; valid edits compile after a short pause.\n-- Write a call in Evaluate tricu expression, then press Evaluate.\n-- Imports/exports declare a module; this introductory one needs no libraries.\n\n-- 1. Definitions give a name to a value: name = expression.\n-- Every value is a tree. These are the three constructor forms.\nleaf = t\nstem = t t\nfork = t t t\n-- Evaluate leaf, stem or fork with tricu · t source to see its structure.\n\n-- 2. A lambda names an input before the colon and returns an expression.\nidentity = (x : x)\n-- Try identity \"Hello!\" with Text, then identity 42 with Natural number.\n\n-- 3. Named parameters are a convenient way to write functions.\n-- This is equivalent to first = (a b : a).\nfirst a b = a\n-- Try first 7 9: it returns the first of its two arguments.\n\n-- 4. Application is written with spaces and groups to the left.\n-- first 7 9 means (first 7) 9. Parentheses group an expression explicitly.\nanswer = identity (first 42 7)\n-- Evaluate answer with Natural number: the result is 42.\n-- A name by itself needs no further arguments; [] is an empty-list value.\n\n-- 5. Numbers, quoted strings and lists are convenient tree literals.\ngreeting = \"Hello from tricu!\"\nnumbers = [1 2 3]\n-- Evaluate greeting with Text, or numbers with Bytes to display [1, 2, 3].\n-- Bytes requires a list of naturals in 0..255; use t source for other lists.\n-- Lists use spaces, not commas: try identity [4 5 6].\n\n-- 6. The result interpretation tells the playground how to read a tree.\n-- Use Text for a string, Natural number for a number, or t source for any tree.\n-- Pure value observes the result; Browser IO interprets an IO action tree.\n-- The Conversation demo shows input, output and continuations.\n\n-- Settings also offers structured arguments and stored-definition inspection.\n-- Edits stay local; Restore original returns to the published source.\n-- For an application spanning lines, put the entire call in parentheses.\n-- Other demos import libraries by immutable manifest hash, shown in their headers.\n-- Next: Tree microscope shows how functions can inspect other program trees.\n", - "sourceSha256": "24835a93f2da82d4b16876074e18634045e750e832a6694bd6067ba78e1a5508", + "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", + "export": "greeting", + "arguments": [], + "output": "string", + "mode": "pure", + "presets": { + "greeting": { + "arguments": [], + "output": "string", + "mode": "pure" + }, + "answer": { + "arguments": [], + "output": "number", + "mode": "pure" + } + }, + "environment": "empty" + }, + { + "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", + "export": "total", + "arguments": [], + "output": "number", + "mode": "pure", + "presets": { + "total": { + "arguments": [], + "output": "number", + "mode": "pure" + }, + "product": { + "arguments": [], + "output": "number", + "mode": "pure" + }, + "grouped": { + "arguments": [], + "output": "number", + "mode": "pure" + } + }, + "environment": "empty" + }, + { + "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" + } + ], + "output": "number", + "mode": "pure", + "presets": { + "double": { + "arguments": [ + { + "type": "number", + "value": "6" + } + ], + "output": "number", + "mode": "pure" + }, + "add": { + "arguments": [ + { + "type": "number", + "value": "4" + }, + { + "type": "number", + "value": "7" + } + ], + "output": "number", + "mode": "pure" + } + }, + "environment": "empty" + }, + { + "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", + "arguments": [], + "output": "number", + "mode": "pure", + "presets": { + "count": { + "arguments": [], + "output": "number", + "mode": "pure" + }, + "total": { + "arguments": [], + "output": "number", + "mode": "pure" + } + }, + "environment": "empty" + }, + { + "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", + "export": "total", + "arguments": [], + "output": "number", + "mode": "pure", + "presets": { + "total": { + "arguments": [], + "output": "number", + "mode": "pure" + }, + "double": { + "arguments": [ + { + "type": "number", + "value": "5" + } + ], + "output": "number", + "mode": "pure" + } + }, + "environment": "empty" + }, + { + "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" + } + ], + "output": "string", + "mode": "pure", + "presets": { + "message": { + "arguments": [ + { + "type": "number", + "value": "0" + } + ], + "output": "string", + "mode": "pure" + } + }, + "environment": "empty" + }, + { + "id": "language-reference", + "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": "5d4035e52ef2f9f85278d6431fc5be43df4e7badce7393930237fc1deb1c4fb8", "export": "answer", "arguments": [], "output": "number", @@ -18,11 +203,11 @@ "identity": { "arguments": [ { - "type": "string", - "value": "Hello!" + "type": "number", + "value": "7" } ], - "output": "string", + "output": "number", "mode": "pure" }, "first": { @@ -39,30 +224,151 @@ "output": "number", "mode": "pure" }, - "greeting": { + "leaf": { "arguments": [], - "output": "string", + "output": "tricu", "mode": "pure" }, - "numbers": { + "shapes": { "arguments": [], - "output": "bytes", + "output": "tricu", "mode": "pure" }, - "leaf": { + "literals": { "arguments": [], "output": "tricu", "mode": "pure" }, - "stem": { - "arguments": [], - "output": "tricu", + "addTwo": { + "arguments": [ + { + "type": "number", + "value": "5" + } + ], + "output": "number", "mode": "pure" }, - "fork": { - "arguments": [], + "describe": { + "arguments": [ + { + "type": "number", + "value": "7" + } + ], + "output": "string", + "mode": "pure" + }, + "keep": { + "arguments": [ + { + "type": "tree", + "value": "200" + } + ], "output": "tricu", "mode": "pure" + }, + "local": { + "arguments": [ + { + "type": "number", + "value": "6" + } + ], + "output": "number", + "mode": "pure" + }, + "viaWhere": { + "arguments": [ + { + "type": "number", + "value": "6" + } + ], + "output": "number", + "mode": "pure" + }, + "copy": { + "arguments": [ + { + "type": "list", + "value": [ + { + "type": "number", + "value": "1" + }, + { + "type": "number", + "value": "2" + }, + { + "type": "number", + "value": "3" + } + ] + } + ], + "output": "bytes", + "mode": "pure" + }, + "firstOr": { + "arguments": [ + { + "type": "number", + "value": "99" + }, + { + "type": "list", + "value": [ + { + "type": "number", + "value": "5" + } + ] + } + ], + "output": "number", + "mode": "pure" + }, + "localCopy": { + "arguments": [ + { + "type": "list", + "value": [ + { + "type": "number", + "value": "4" + }, + { + "type": "number", + "value": "5" + } + ] + } + ], + "output": "bytes", + "mode": "pure" + }, + "checked": { + "arguments": [ + { + "type": "number", + "value": "42" + } + ], + "output": "number", + "mode": "pure" + }, + "sequenced": { + "arguments": [ + { + "type": "number", + "value": "9" + } + ], + "output": "number", + "mode": "pure" } }, "environment": "empty" @@ -70,6 +376,7 @@ { "id": "greet", "title": "A conversation", + "category": "Practical Examples", "hash": "0811457d501113ee11194f90e279ebb4ef9d548c70cb48d146be03eb9af7505f", "source": "!import 2d23be85d277457fd36c84f5eaa227e94e1c4efec281ff73017aaee109096f49 as IO\n!export main\n\n-- A conversation built from the IO library's actions and explicit bind operator.\n-- Evaluate main in Browser IO mode, then answer in the terminal input.\n-- The returned name is also displayed as Text. Try changing the greeting.\n-- Importing IO does not grant filesystem or network access in the browser.\nmain = (IO.io (do IO.bind\n IO.putStr \"What is your name?\\n\"\n name <- IO.getLine\n IO.putStr \"Hello, \"\n IO.putStr name\n IO.putStr \"!\\n\"\n IO.pure name))\n", "sourceSha256": "3b9835aec1f3b987ad954cd235a7c0517170d3f7e236b15d60ce9a3a809c74c4", @@ -83,6 +390,7 @@ { "id": "microscope", "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 : count right (count left (B.succ acc)))\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": "0dd0bb5444662200cf3556dd7a81e65570b5d3b34d4b3e6b17e6d9c36d3d1114", @@ -136,6 +444,7 @@ { "id": "rgb", "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", @@ -158,6 +467,7 @@ { "id": "shipping", "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": "d6d9378ab39733a8aabaf4f6a25851350e99fa81607bf05a64266301e2712f5a", @@ -221,6 +531,7 @@ { "id": "events", "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": "ca11d9a7e20bfe228545c43f50aed57089c020eead516e23b67d909fae8923de", @@ -299,6 +610,7 @@ { "id": "registration", "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": "531866c6b2e7e42ecbd4ae285c502b59321ad31fd2b82b36d8a139938592ac31", @@ -352,6 +664,7 @@ { "id": "migration", "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": "66ee7bc1a9005b5f1d12f168b7866231ad7f523cf34f52b63958cb155a5c49b5", @@ -453,7 +766,8 @@ "output": "record", "mode": "pure" } - } + }, + "category": "Language & Runtime Explorations" }, { "id": "lexer", @@ -493,6 +807,7 @@ "output": "record", "mode": "pure" } - } + }, + "category": "Language & Runtime Explorations" } ] diff --git a/ext/demo-site/demos.json b/ext/demo-site/demos.json @@ -7,6 +7,445 @@ "compilerManifest": "17df7d4ef56684d423ca59d5f4c4d03aa73b24c5579e00f8719e2f06641c39e6", "hostSha256": "c1eea492839b4425f285dd3a0765bbb8bdc323d2320a2181760d34180ee15073", "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", + "mode": "pure", + "haskellCases": [ + { + "export": "greeting", + "arguments": [], + "input": null, + "terminal": "", + "result": "220202021020202100221020210202021021002202021021020210210022020210210202102100221021021021020210210022020210210202100220202020202100221021021020210210210022102102102102021021002202102020210210210022020210210202102100220202102020210210022102020202021000" + }, + { + "export": "answer", + "arguments": [], + "input": null, + "terminal": "", + "result": "2021020210202100" + } + ] + }, + "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", + "mode": "pure", + "haskellCases": [ + { + "export": "total", + "arguments": [], + "input": null, + "terminal": "", + "result": "20202102100" + }, + { + "export": "product", + "arguments": [], + "input": null, + "terminal": "", + "result": "2102102020202100" + }, + { + "export": "grouped", + "arguments": [], + "input": null, + "terminal": "", + "result": "2020202102100" + } + ] + }, + "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", + "mode": "pure", + "haskellCases": [ + { + "export": "double", + "arguments": [ + "202102100" + ], + "input": null, + "terminal": "", + "result": "20202102100" + }, + { + "export": "double", + "arguments": [ + "21020202100" + ], + "input": null, + "terminal": "", + "result": "2021020202100" + }, + { + "export": "add", + "arguments": [ + "20202100", + "2102102100" + ], + "input": null, + "terminal": "", + "result": "210210202100" + } + ] + }, + "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", + "mode": "pure", + "haskellCases": [ + { + "export": "count", + "arguments": [], + "input": null, + "terminal": "", + "result": "2102100" + }, + { + "export": "total", + "arguments": [], + "input": null, + "terminal": "", + "result": "20202102100" + } + ] + }, + "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", + "mode": "pure", + "haskellCases": [ + { + "export": "total", + "arguments": [], + "input": null, + "terminal": "", + "result": "2020202102100" + }, + { + "export": "double", + "arguments": [ + "210202100" + ], + "input": null, + "terminal": "", + "result": "20210202100" + } + ] + }, + "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", + "mode": "pure", + "haskellCases": [ + { + "export": "message", + "arguments": [ + "0" + ], + "input": null, + "terminal": "", + "result": "2210202021021020210022102102102102021021002210202102021021021002202102020210210210022020202020210022021020202021021002210202020202102100221021020202102102100221021020210202102100221020210202021021002202021020210210210022020202020210022102020210202102100221021020202102102100220202020202100221020210202021021002210202102102021021002202020202102102100220202102021021021002210202021021021021002202102102102021000" + }, + { + "export": "message", + "arguments": [ + "2100" + ], + "input": null, + "terminal": "", + "result": "22102020210210202100221021021021020210210022102021020210210210022020202020210022020202102021021002210202020202102100220210210202102102100221020210202021021002202020202021002210210210210202102100220210210210202102100221020210202021021002202020202021002210202021020210210022020210202102102100221020210202021021002210202102102021021002202102102102021000" + }, + { + "export": "message", + "arguments": [ + "20202100" + ], + "input": null, + "terminal": "", + "result": "22102020210210202100221021021021020210210022102021020210210210022020202020210022020202102021021002210202020202102100220210210202102102100221020210202021021002202020202021002210210202021021021002210202102020210210022021021020210210210022102021020202102100220210202021021021002210202020202102100220202102102021021002202020202021002210202021020210210022020210202102102100221020210202021021002210202102102021021002210210202021021021002202102102102021000" + } + ] + }, + "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": "5d4035e52ef2f9f85278d6431fc5be43df4e7badce7393930237fc1deb1c4fb8", + "hash": "86717d7f9d69fe730c6c6172c42c49895623aa070bf675e1b5f751adb30616c1", + "mode": "pure", + "haskellCases": [ + { + "export": "answer", + "arguments": [], + "input": null, + "terminal": "", + "result": "2021020210202100" + }, + { + "export": "identity", + "arguments": [ + "2102102100" + ], + "input": null, + "terminal": "", + "result": "2102102100" + }, + { + "export": "identity", + "arguments": [ + "220202021020210210022102021020202102100220202102102021021002202021021020210210022102102102102021021000" + ], + "input": null, + "terminal": "", + "result": "220202021020210210022102021020202102100220202102102021021002202021021020210210022102102102102021021000" + }, + { + "export": "first", + "arguments": [ + "2102102100", + "21020202100" + ], + "input": null, + "terminal": "", + "result": "2102102100" + }, + { + "export": "leaf", + "arguments": [], + "input": null, + "terminal": "", + "result": "0" + }, + { + "export": "shapes", + "arguments": [], + "input": null, + "terminal": "", + "result": "2021022000" + }, + { + "export": "literals", + "arguments": [], + "input": null, + "terminal": "", + "result": "22202020210202102100221020210202021021002202021021020210210022020210210202102100221021021021020210210002202102021020210022210022021002210210000" + }, + { + "export": "addTwo", + "arguments": [ + "210202100" + ], + "input": null, + "terminal": "", + "result": "2102102100" + }, + { + "export": "describe", + "arguments": [ + "2102102100" + ], + "input": null, + "terminal": "", + "result": "22102102020210210210022102021020202102100220210210202102102100221020210202021021002202102102102021021000" + }, + { + "export": "describe", + "arguments": [ + "2202020210202102100221020202102021021000" + ], + "input": null, + "terminal": "", + "result": "22102102102020210210022021020202102102100221020210202021021002210202102020210210022020210202102102100221020202102021021002202102102102021021002210210210202021021000" + }, + { + "export": "describe", + "arguments": [ + "10" + ], + "input": null, + "terminal": "", + "result": "2202021020210210210022021020202102102100221020210202102102100221020210202021021000" + }, + { + "export": "describe", + "arguments": [ + "22020210022102021000" + ], + "input": null, + "terminal": "", + "result": "22020210202102102100221021021020210210210022102102102102021021002202020202021002210202021020210210022020210202102102100221020210202021021002210202102102021021002210210202021021021000" + }, + { + "export": "describe", + "arguments": [ + "221002202100221021000" + ], + "input": null, + "terminal": "", + "result": "2202102102102021021002210210210210202102100220210210210202102100221020210202021021002210202102102021021002202020202102102100220202102021021021002210202021021021021002202020202021002202021021020210210022102020210202102100221021020202102102100220202102021021021000" + }, + { + "export": "describe", + "arguments": [ + "0" + ], + "input": null, + "terminal": "", + "result": "221020210202021021002210202102102021021002202020202102102100220202102021021021002210202021021021021002202020202021002202021021020210210022102020210202102100221021020202102102100220202102021021021000" + }, + { + "export": "describe", + "arguments": [ + "110" + ], + "input": null, + "terminal": "", + "result": "221021021021020210210022020210202102102100220202021020210210022102021020202102100220210202021021021000" + }, + { + "export": "keep", + "arguments": [ + "200" + ], + "input": null, + "terminal": "", + "result": "200" + }, + { + "export": "keep", + "arguments": [ + "10" + ], + "input": null, + "terminal": "", + "result": "0" + }, + { + "export": "local", + "arguments": [ + "202102100" + ], + "input": null, + "terminal": "", + "result": "20202102100" + }, + { + "export": "viaWhere", + "arguments": [ + "202102100" + ], + "input": null, + "terminal": "", + "result": "2102102100" + }, + { + "export": "copy", + "arguments": [ + "221002202100221021000" + ], + "input": null, + "terminal": "", + "result": "221002202100221021000" + }, + { + "export": "copy", + "arguments": [ + "0" + ], + "input": null, + "terminal": "", + "result": "0" + }, + { + "export": "copy", + "arguments": [ + "10" + ], + "input": null, + "terminal": "", + "result": "0" + }, + { + "export": "firstOr", + "arguments": [ + "2102102020202102100", + "22102021000" + ], + "input": null, + "terminal": "", + "result": "210202100" + }, + { + "export": "firstOr", + "arguments": [ + "2102102020202102100", + "0" + ], + "input": null, + "terminal": "", + "result": "2102102020202102100" + }, + { + "export": "localCopy", + "arguments": [ + "22020210022102021000" + ], + "input": null, + "terminal": "", + "result": "22020210022102021000" + }, + { + "export": "localCopy", + "arguments": [ + "0" + ], + "input": null, + "terminal": "", + "result": "0" + }, + { + "export": "localCopy", + "arguments": [ + "10" + ], + "input": null, + "terminal": "", + "result": "0" + }, + { + "export": "checked", + "arguments": [ + "2021020210202100" + ], + "input": null, + "terminal": "", + "result": "2021020210202100" + }, + { + "export": "sequenced", + "arguments": [ + "21020202100" + ], + "input": null, + "terminal": "", + "result": "21020202100" + } + ] + }, "intro": { "demandCases": [], "source": "!export *\n\n-- Start here: a small tricu language guide.\n-- Lines beginning with -- are comments. Read down, then try the exports.\n-- Edit this source in place; valid edits compile after a short pause.\n-- Write a call in Evaluate tricu expression, then press Evaluate.\n-- Imports/exports declare a module; this introductory one needs no libraries.\n\n-- 1. Definitions give a name to a value: name = expression.\n-- Every value is a tree. These are the three constructor forms.\nleaf = t\nstem = t t\nfork = t t t\n-- Evaluate leaf, stem or fork with tricu · t source to see its structure.\n\n-- 2. A lambda names an input before the colon and returns an expression.\nidentity = (x : x)\n-- Try identity \"Hello!\" with Text, then identity 42 with Natural number.\n\n-- 3. Named parameters are a convenient way to write functions.\n-- This is equivalent to first = (a b : a).\nfirst a b = a\n-- Try first 7 9: it returns the first of its two arguments.\n\n-- 4. Application is written with spaces and groups to the left.\n-- first 7 9 means (first 7) 9. Parentheses group an expression explicitly.\nanswer = identity (first 42 7)\n-- Evaluate answer with Natural number: the result is 42.\n-- A name by itself needs no further arguments; [] is an empty-list value.\n\n-- 5. Numbers, quoted strings and lists are convenient tree literals.\ngreeting = \"Hello from tricu!\"\nnumbers = [1 2 3]\n-- Evaluate greeting with Text, or numbers with Bytes to display [1, 2, 3].\n-- Bytes requires a list of naturals in 0..255; use t source for other lists.\n-- Lists use spaces, not commas: try identity [4 5 6].\n\n-- 6. The result interpretation tells the playground how to read a tree.\n-- Use Text for a string, Natural number for a number, or t source for any tree.\n-- Pure value observes the result; Browser IO interprets an IO action tree.\n-- The Conversation demo shows input, output and continuations.\n\n-- Settings also offers structured arguments and stored-definition inspection.\n-- Edits stay local; Restore original returns to the published source.\n-- For an application spanning lines, put the entire call in parentheses.\n-- Other demos import libraries by immutable manifest hash, shown in their headers.\n-- Next: Tree microscope shows how functions can inspect other program trees.\n", diff --git a/ext/demo-site/demos/arithmetic.tri b/ext/demo-site/demos/arithmetic.tri @@ -0,0 +1,19 @@ +!import 7f87549d587fa8c3be88af0ea988db3adcc3b35f1ffb48973553491b075614db as Math +!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 + +-- Parentheses make one calculation the input to another. +grouped = Math.multiply (Math.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. +-- Next: Your first function. diff --git a/ext/demo-site/demos/choices.tri b/ext/demo-site/demos/choices.tri @@ -0,0 +1,15 @@ +!export message + +-- Choose an answer 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. + +-- Exercise: add 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. diff --git a/ext/demo-site/demos/functions.tri b/ext/demo-site/demos/functions.tri @@ -0,0 +1,19 @@ +!import 7f87549d587fa8c3be88af0ea988db3adcc3b35f1ffb48973553491b075614db as Math +!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. + +-- number is a parameter: each call supplies its value. +-- Try double 9: the same definition now returns 18. + +-- 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. + +-- 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. +-- Next: Working with lists. diff --git a/ext/demo-site/demos/language-reference.tri b/ext/demo-site/demos/language-reference.tri @@ -0,0 +1,94 @@ +!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 * +!import 7f87549d587fa8c3be88af0ea988db3adcc3b35f1ffb48973553491b075614db as Math +!export * + +-- 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. +-- Full contracts: follow For AI assistants in the footer to Language features. +-- Each comment below gives an expression and its interpretation. + +-- Names, lambdas, named parameters, application and grouping. +identity = (x : x) +first a b = a +answer = identity (first 42 7) +-- identity "hello" / Text -> hello; first 7 9 / Natural number -> 7. + +-- The kernel constructors: Leaf, Stem and Fork. Use tricu for arbitrary trees. +leaf = t +shapes = [t (t t) (t t t)] +-- leaf / tricu -> t; shapes / tricu shows a list containing all three shapes. +literals = ["hello" 42 [1 2 3]] +-- literals / tricu shows mixed values. Strings support Unicode; names are ASCII. +-- Lists use spaces. Group application items: [(identity 1) (identity 2)]. + +-- Partial application: provide one of a function's arguments now. +addTwo = Math.add 2 +-- addTwo 5 / Natural number -> 7. + +-- Ordered equations: literals, a qualified pin, exact lists, cons and wildcard. +-- A pin (^true) compares against an existing value. +describe 7 = "seven" +describe "hi" = "greeting" +describe ^true = "true" +describe [a b] = "two items" +describe [head | tail] = "nonempty list" +describe [] = "empty list" +describe _ = "other" +-- Try each with Text: describe 7, describe "hi", describe (t t), +-- describe [4 5], describe [1 2 3], describe [], describe (t (t t)). + +-- Pair patterns and whole-value capture. Construct a pair with t left right. +keep whole{left, right} = whole +keep _ = t +-- keep (t t t) / tricu -> (t t t); keep (t t) / tricu -> t. + +-- Plain local bindings are sequential and nonrecursive. +local x = (let + double n = Math.multiply n 2 + in double x) +viaWhere x = step x where step y = Math.add y 1 +-- local 6 -> 12; viaWhere 6 -> 7, both Natural number. + +-- Direct self-recursion, with a final fallback equation. +copy [] = [] +copy [head | tail] = t head (copy tail) +copy _ = [] +-- copy [1 2 3] / Bytes -> [1, 2, 3]. Bytes displays naturals from 0 through 255. +-- Use tricu for a general list; copy [] and copy (t t) both return []. + +-- case examines an intermediate value; arms align and finish with a fallback. +firstOr fallback xs = case xs of + [head | _] = head + _ = fallback +-- firstOr 99 [5] -> 5; firstOr 99 [] -> 99, both Natural number. + +-- Local self-recursion is explicit: let rec (also available with where rec). +localCopy xs = (let rec + walk [] = [] + walk [head | tail] = t head (walk tail) + walk _ = [] + in walk xs) +-- localCopy [4 5] / Bytes -> [4, 5]; localCopy [] -> []. + +-- Parameter and result contracts check values at runtime. +checked x@nat? =@nat? x +-- checked 42 / Natural number -> 42. A failed contract produces a Result. +-- Keep annotations on plain function heads; see Contracts for Result handlers. + +-- Pipes and do use an explicit sequencing operator. Here a lambda passes a value +-- to the next continuation. It is ordinary pure code; IO uses IO.bind instead. +sequenced input = (do (value next : next value) + value <- input |> identity + identity value + identity <| value) +-- sequenced 9 / Natural number -> 9. +-- Both pipe chains associate left-to-right; group nested applications explicitly. + +|- 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. +-- 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 @@ -0,0 +1,17 @@ +!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 as List +!export count +!export total + +-- Collect values in a list. +numbers = [2 4 6] +count = List.length numbers +total = List.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. +-- Next: Transforming a list. diff --git a/ext/demo-site/demos/names-values.tri b/ext/demo-site/demos/names-values.tri @@ -0,0 +1,18 @@ +!export greeting +!export answer + +-- Give a value a name. +greeting = "Hello, world!" +-- Evaluate greeting with Text. +-- Result: Hello, world! +-- Try changing the words, then Evaluate again. + +-- Numbers can have names too. +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. diff --git a/ext/demo-site/demos/transform-list.tri b/ext/demo-site/demos/transform-list.tri @@ -0,0 +1,18 @@ +!import bd2c14d6db6b69308a5252a8e839705a55d30e815f012900b094315263e08d81 as List +!import 7f87549d587fa8c3be88af0ea988db3adcc3b35f1ffb48973553491b075614db as Math +!export total +!export double + +-- Apply the same function to every item. +double number = Math.multiply 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. + +-- Try double 5 with Natural number: the result is 10. +-- Exercise: make double multiply by 3. The new total should be 36. +-- Next: Making a choice. diff --git a/ext/demo-site/dist/app.js b/ext/demo-site/dist/app.js @@ -1,3 +1,4 @@ +import { exampleGroups, exampleGroup, selectorLibraries, libraryLabel } from './program-catalogue.js'; import { importPicker } from './import-picker.js'; import { localModulesUI } from './local-modules-ui.js'; import { outputReveal } from './output-reveal.js'; @@ -645,27 +646,31 @@ function populatePrograms() { placeholder.disabled = true; placeholder.textContent = 'Choose a program'; programPicker.append(placeholder); - const demos = document.createElement('optgroup'); - demos.label = 'Examples'; - for (const entry of entries) { - const option = document.createElement('option'); - option.value = `demo:${entry.id}`; - option.textContent = entry.title; - demos.append(option); + for (const category of exampleGroups) { + const group = document.createElement('optgroup'); + group.label = category; + for (const entry of entries.filter(entry => exampleGroup(entry) === category)) { + const option = document.createElement('option'); + option.value = `demo:${entry.id}`; + option.textContent = entry.title; + group.append(option); + } + if (group.children.length) + programPicker.append(group); } const packages = document.createElement('optgroup'); - packages.label = 'Published packages'; - const exampleNames = new Set(entries.map(examplePackage)); - for (const name of new Set(releases.map(r => r.name).filter(name => !exampleNames.has(name)))) { + packages.label = 'Libraries'; + for (const release of selectorLibraries(releases)) { const option = document.createElement('option'); - option.value = `package:${name}`; - option.textContent = name; + option.value = `package:${release.name}`; + option.textContent = libraryLabel(release.name); + option.title = release.title; packages.append(option); } const hash = document.createElement('option'); hash.value = 'hash'; hash.textContent = 'Load by bundle hash…'; - programPicker.append(demos, packages, hash); + programPicker.append(packages, hash); programPicker.disabled = false; } function showRelease(release) { @@ -1130,6 +1135,8 @@ async function initializePackages() { outputType(v['output']); if (v['mode'] !== 'pure' && v['mode'] !== 'io') throw new Error('Invalid catalogue mode'); + if (v['category'] !== undefined && !exampleGroups.includes(v['category'])) + throw new Error('Invalid example category'); if (v['environment'] !== undefined && (typeof v['environment'] !== 'string' || v['environment'].length > 128)) throw new Error('Invalid demo environment'); return { ...v, hash }; @@ -1142,7 +1149,7 @@ async function initializePackages() { return; } if (!loadFragment()) { - const initial = entries.find(e => e.id === 'intro') ?? entries[0]; + const initial = entries.find(e => e.id === 'names-values') ?? entries[0]; if (!initial) throw new Error('Empty catalogue'); selectExample(initial); diff --git a/ext/demo-site/dist/demo-inputs.js b/ext/demo-site/dist/demo-inputs.js @@ -2,7 +2,15 @@ // The argument order comes from the published demos beside this site. export function demoInputLabels(id, definition) { switch (id) { - case 'intro': return definition === 'first' ? ['First value', 'Second value'] : definition === 'identity' ? ['Value'] : []; + case 'functions': return definition === 'add' ? ['Left number', 'Right number'] : ['Number']; + case 'transform-list': return definition === 'double' ? ['Number'] : []; + case 'choices': return ['Number of items']; + case 'language-reference': + if (definition === 'first') + return ['First value', 'Second value']; + if (definition === 'firstOr') + return ['Fallback', 'List']; + return ['identity', 'addTwo', 'describe', 'keep', 'local', 'viaWhere', 'copy', 'localCopy', 'checked', 'sequenced'].includes(definition) ? ['Value'] : []; case 'microscope': return definition === 'same' ? ['First tree', 'Second tree'] : ['Tree to inspect']; case 'rgb': return ['RGB channels (0–255)']; case 'shipping': return ['Destination', 'Cart subtotal (cents)', 'Package weight (grams)']; diff --git a/ext/demo-site/dist/program-catalogue.js b/ext/demo-site/dist/program-catalogue.js @@ -0,0 +1,27 @@ +import { packageReleases } from './packages.js'; +export const exampleGroups = ['Basic Examples', 'Practical Examples', 'Language & Runtime Explorations']; +export function exampleGroup(entry) { + return entry.category ?? (['microscope', 'parser', 'lexer'].includes(entry.id) ? 'Language & Runtime Explorations' : 'Practical Examples'); +} +export function libraryLabel(name) { + if (name === 'tricu-lib-packages-base') + return 'base · convenience interface'; + if (name === 'tricu-lib-packages-io') + return 'io · convenience interface'; + if (name === 'tricu-compiler') + return 'compiler · combined toolchain'; + if (name === 'tricu-source-builder') + return 'source-builder · native entrypoint'; + return name.replace(/^tricu-lib-/u, '').replace(/^tricu-/u, ''); +} +// Publication names may be aliases of exactly the same native module. Prefer +// filesystem library publications, while keeping distinct interfaces separate. +// Historical releases remain in the registry and reachable by bundle hash. +export function selectorLibraries(releases) { + const latest = [...new Set(releases.map(r => r.name))].map(name => packageReleases(releases, name)[0]).filter(r => !r.demo); + latest.sort((a, b) => Number(b.name.startsWith('tricu-lib-')) - Number(a.name.startsWith('tricu-lib-')) || a.name.localeCompare(b.name)); + const manifests = new Set(); + return latest.filter(r => { if (manifests.has(r.manifest)) + return false; manifests.add(r.manifest); return true; }) + .sort((a, b) => libraryLabel(a.name).localeCompare(libraryLabel(b.name))); +} diff --git a/ext/demo-site/index.html b/ext/demo-site/index.html @@ -38,10 +38,10 @@ </section> <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 Start Here…</option></select></div> + <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><option value="zig-wasm">Zig</option></select></div> <div class="toolbar-actions"><button id="stop" disabled hidden>Stop</button><button id="settings-toggle" aria-controls="settings-panel" aria-expanded="false">Settings</button></div> - <span id="status" role="status">Loading Start Here</span> + <span id="status" role="status">Loading Names and values</span> </div> <div id="editor-body" class="editor-body"> <div class="editor-main" data-inspector-background> @@ -50,7 +50,7 @@ <p id="authorship">Published package</p><p id="source-availability" class="hint" hidden></p> <label class="sr-only" for="source-editor">tricu source (up to 16,384 characters)</label> <textarea id="source-editor" rows="18" maxlength="32768" spellcheck="false" autocapitalize="off" autocorrect="off" hidden></textarea> - <pre id="source-notes"><code id="source">Loading Start Here…</code></pre> + <pre id="source-notes"><code id="source">Loading Names and values…</code></pre> <p id="compile-diagnostic" class="error" role="status" hidden></p><p id="compile-activity" class="hint" role="status" hidden></p> <div class="source-actions"><button id="add-import" disabled hidden>Add import…</button><button id="restore-source" disabled hidden>Restore published source</button><span class="hint">Edits compile after a pause. Evaluation is always explicit.</span></div> </section> diff --git a/ext/demo-site/packages.json b/ext/demo-site/packages.json @@ -1337,6 +1337,62 @@ "manifest": "0713754ddf34e447f3581fc105226da560f0c6533f1e08b666057e061866b600", "title": "Profile migrations", "demo": "migration" + }, + { + "name": "names-values", + "version": "0.1.0", + "hash": "8d1b7e1f6dfefe983167ee8d9631476caaa0c3dc689a85628e9ca627ebaef6d0", + "manifest": "e30e884e7925c755b111c5a753d78fcb5bafeb9f431f7261f4d7e1e905faba7d", + "title": "Names and values", + "demo": "names-values" + }, + { + "name": "arithmetic", + "version": "0.1.0", + "hash": "38110ab45a75a9c8771d6422277cb3083e667a4368aa33df0b43af672f17375e", + "manifest": "a07edd4c204a3ac8ddc20764de23857bcf9906c4c69d0f5706ad2fe0518f3646", + "title": "Doing arithmetic", + "demo": "arithmetic" + }, + { + "name": "functions", + "version": "0.1.0", + "hash": "be5f4675c9b0b6d9c093fe44a9695da9f428c88d62b0f4f1340017067fa81fa9", + "manifest": "819a33501800bcf6ade9ab179b550485c118361b4bf6143da830a62e6a062964", + "title": "Your first function", + "demo": "functions" + }, + { + "name": "lists", + "version": "0.1.0", + "hash": "3cc5d36c5b70a20362117e3349aa044efca8adca56105424965065395a9f3e76", + "manifest": "449e93952f46fe794539e588aaf7f6355ac50c791563505a24e3d8824845af52", + "title": "Working with lists", + "demo": "lists" + }, + { + "name": "transform-list", + "version": "0.1.0", + "hash": "9f10d772b42628acfe843eec7524d8de1ba9ea4344d3952c6c091c98c4d1c72d", + "manifest": "e82efc3f0168a2967dfcab62acbff785012402fc6e316adb73e1978f05b1d4be", + "title": "Transforming a list", + "demo": "transform-list" + }, + { + "name": "choices", + "version": "0.1.0", + "hash": "1972c98bfe599fef2a72cc946401a5cc9280026a8c46fdd7c8ccdc7093ee90cc", + "manifest": "381360c28e8204def839f99608e785962aca8625f77dfeabe083b3db65ec3cab", + "title": "Making a choice", + "demo": "choices" + }, + { + "name": "language-reference", + "version": "0.1.0", + "hash": "86717d7f9d69fe730c6c6172c42c49895623aa070bf675e1b5f751adb30616c1", + "manifest": "9984385a5ab9b81094343d2b4203e825d44c07de67f0e399ad55660e5e431998", + "title": "Language reference", + "demo": "language-reference" } ] } diff --git a/ext/demo-site/prepare-showcase.py b/ext/demo-site/prepare-showcase.py @@ -313,7 +313,7 @@ def main(): release = published('parser-demo', 'Parser combinators: complete input, prefixes and committed errors', parser_bundle, 'parser') source = (HERE / 'demos/parser.tri').read_bytes() preset = dict(arguments=[dict(type='string', value='red,green,blue')], output='record', mode='pure') - demo = dict(id='parser', preparation='native', environment='empty', title='Parser combinators', hash=release['hash'], source=source.decode(), + demo = dict(id='parser', preparation='native', environment='empty', category='Language & Runtime Explorations', title='Parser combinators', hash=release['hash'], source=source.decode(), sourceSha256=hashlib.sha256(source).hexdigest(), export='parseNames', **preset, presets={name: preset for name in ['parseNames', 'parsePrefix']}) goldens = dict(id='parser', source=source.decode(), hash=release['hash'], mode='pure', compilerSha256=metadata['sha256'], @@ -330,7 +330,7 @@ def main(): source = (HERE / 'demos/lexer.tri').read_bytes() word = dict(arguments=[dict(type='string', value='hello,world')], output='record', mode='pure') whitespace = dict(arguments=[dict(type='string', value=' \n\tword')], output='record', mode='pure') - demos.append(dict(id='lexer', preparation='native', environment='empty', title='Located lexer scans', hash=release['hash'], source=source.decode(), + demos.append(dict(id='lexer', preparation='native', environment='empty', category='Language & Runtime Explorations', title='Located lexer scans', hash=release['hash'], source=source.decode(), sourceSha256=hashlib.sha256(source).hexdigest(), export='scanWord', **word, presets=dict(scanWord=word, scanWhitespace=whitespace))) vectors.append(dict(id='lexer', source=source.decode(), hash=release['hash'], mode='pure', compilerSha256=metadata['sha256'], diff --git a/ext/demo-site/prepare.py b/ext/demo-site/prepare.py @@ -99,6 +99,7 @@ def of_string(text): def main(): import practical + import tutorials parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('tricu', type=Path) parser.add_argument('--backend', required=True, choices=['stable']) @@ -114,7 +115,7 @@ def main(): store = temp / 'store' def run(*command, input=None): - reply = subprocess.run([str(host), *map(str, command), '+RTS', '-M8192m', '-RTS'], cwd=ROOT, + reply = subprocess.run([str(host), *map(str, command), '+RTS', '-N1', '-M8192m', '-RTS'], cwd=ROOT, input=input, capture_output=True, timeout=120) if reply.returncode: raise RuntimeError(reply.stderr.decode() + reply.stdout.decode()) @@ -135,6 +136,7 @@ def main(): def of_list(values): return ''.join('2' + value for value in values) + '0' + # The retired intro remains an independently checked runtime fixture. settings = { 'intro': dict(title='Start here', exports=['answer', 'identity', 'first', 'leaf', 'stem', 'fork', 'greeting', 'numbers'], export='answer', args=[], output='number', mode='pure'), @@ -144,20 +146,14 @@ def main(): 'rgb': dict(title='RGB validator', exports=['main'], export='main', args=[dict(type='bytes', value=[64, 128, 255])], output='string', mode='pure'), } - settings['intro']['presets'] = { - 'answer': dict(arguments=[], output='number', mode='pure'), - 'identity': dict(arguments=[dict(type='string', value='Hello!')], output='string', mode='pure'), - 'first': dict(arguments=[dict(type='number', value='7'), dict(type='number', value='9')], output='number', mode='pure'), - 'greeting': dict(arguments=[], output='string', mode='pure'), - 'numbers': dict(arguments=[], output='bytes', mode='pure'), - **{name: dict(arguments=[], output='tricu', mode='pure') for name in ['leaf', 'stem', 'fork']}, - } settings['microscope']['presets'] = { 'shape': dict(arguments=[dict(type='tree', value='21100')], output='string', mode='pure'), 'size': dict(arguments=[dict(type='tree', value='21100')], output='number', mode='pure'), 'same': dict(arguments=[dict(type='tree', value='21100'), dict(type='tree', value='21100')], output='bool', mode='pure'), } settings.update(practical.settings()) + settings = {**tutorials.settings(), **settings} + tutorial_cases = tutorials.cases() practical_cases = practical.cases() def compile_unit(source, environment): env_file = temp / 'environment.tree' @@ -237,6 +233,9 @@ def main(): if name in practical_cases: for export, arguments, record in practical_cases[name]: check(export, [encode_literal(v) for v in arguments], encode_record(record), record=record) + elif name in tutorial_cases: + for export, arguments, expected in tutorial_cases[name]: + check(export, [encode_literal(a) for a in arguments], encode_literal(expected)) elif name == 'intro': check('answer', [], of_number(42)) for tree in ['0', '10', '200', of_number(7), of_string('Hello!')]: @@ -290,10 +289,11 @@ def main(): demand_cases.append(dict(label=label, source=probe_source.decode(), program=probe, result=expected)) generated[name] = dict(demandCases=demand_cases, source=source.decode(), sourceSha256=digest(source), hash=bundle_hash, mode=setting['mode'], haskellCases=cases) - catalogue.append(dict(id=name, title=setting['title'], hash=bundle_hash, - source=source.decode(), - sourceSha256=digest(source), export=setting['export'], arguments=setting['args'], - output=setting['output'], mode=setting['mode'], presets=setting.get('presets', {}))) + if name != 'intro': + catalogue.append(dict(id=name, title=setting['title'], category=setting.get('category', 'Language & Runtime Explorations' if name == 'microscope' else 'Practical Examples'), hash=bundle_hash, + source=source.decode(), + sourceSha256=digest(source), export=setting['export'], arguments=setting['args'], + output=setting['output'], mode=setting['mode'], presets=setting.get('presets', {}))) print(f'{name}: stable bundle {len(bundle)} bytes; {len(cases)} Haskell goldens') # Portable-authored, Haskell-serialized codec vectors, independent of the JS codec. diff --git a/ext/demo-site/reference-input/getting-started.md b/ext/demo-site/reference-input/getting-started.md @@ -4,9 +4,14 @@ tricu is the source language. Arboricx bundles carry its compiled, named tree values between runtimes. The playground at [arbori.cx](https://arbori.cx/) lets you edit modules and evaluate expressions using Haskell or Zig WebAssembly. +**Basic Examples** starts with Names and values, then introduces arithmetic, +functions, lists, transformations and choices. Each lesson includes an expression, +expected result and small edit exercise. For a compact syntax tour, select +**Language & Runtime Explorations → Language reference**. + ## A complete first program -Choose **Start Here** in the Program selector. Replace its Source with: +Choose **Names and values** in the Program selector. Replace its Source with: ```tricu !export answer diff --git a/ext/demo-site/src/app.ts b/ext/demo-site/src/app.ts @@ -1,3 +1,4 @@ +import { exampleGroups, exampleGroup, selectorLibraries, libraryLabel } from './program-catalogue.js'; import { importPicker } from './import-picker.js'; import { localModulesUI } from './local-modules-ui.js'; import { outputReveal } from './output-reveal.js'; @@ -408,17 +409,20 @@ function chooseProgram(value: string, version?: string): void { function populatePrograms(): void { programPicker.replaceChildren(); const placeholder = document.createElement('option'); placeholder.value = ''; placeholder.disabled = true; placeholder.textContent = 'Choose a program'; programPicker.append(placeholder); - const demos = document.createElement('optgroup'); demos.label = 'Examples'; - for (const entry of entries) { - const option = document.createElement('option'); option.value = `demo:${entry.id}`; option.textContent = entry.title; demos.append(option); + for (const category of exampleGroups) { + const group = document.createElement('optgroup'); group.label = category; + for (const entry of entries.filter(entry => exampleGroup(entry) === category)) { + const option = document.createElement('option'); option.value = `demo:${entry.id}`; option.textContent = entry.title; group.append(option); + } + if (group.children.length) programPicker.append(group); } - const packages = document.createElement('optgroup'); packages.label = 'Published packages'; - const exampleNames = new Set(entries.map(examplePackage)); - for (const name of new Set(releases.map(r => r.name).filter(name => !exampleNames.has(name)))) { - const option = document.createElement('option'); option.value = `package:${name}`; option.textContent = name; packages.append(option); + const packages = document.createElement('optgroup'); packages.label = 'Libraries'; + for (const release of selectorLibraries(releases)) { + const option = document.createElement('option'); option.value = `package:${release.name}`; + option.textContent = libraryLabel(release.name); option.title = release.title; packages.append(option); } const hash = document.createElement('option'); hash.value = 'hash'; hash.textContent = 'Load by bundle hash…'; - programPicker.append(demos, packages, hash); programPicker.disabled = false; + programPicker.append(packages, hash); programPicker.disabled = false; } function showRelease(release?: Release): void { el('module-import-help').hidden = release === undefined; @@ -668,6 +672,7 @@ async function initializePackages(): Promise<void> { for (const key of ['id', 'title', 'source', 'sourceSha256', 'export']) if (typeof v[key] !== 'string') throw new Error('Invalid catalogue field'); const hash = bundleHash(v['hash']); literals(v['arguments']); outputType(v['output']); if (v['mode'] !== 'pure' && v['mode'] !== 'io') throw new Error('Invalid catalogue mode'); + if (v['category'] !== undefined && !exampleGroups.includes(v['category'] as typeof exampleGroups[number])) throw new Error('Invalid example category'); if (v['environment'] !== undefined && (typeof v['environment'] !== 'string' || v['environment'].length > 128)) throw new Error('Invalid demo environment'); return { ...v, hash } as unknown as CatalogueEntry; }); @@ -675,7 +680,7 @@ async function initializePackages(): Promise<void> { releases = packageIndex(JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(indexBytes)) as unknown); populatePrograms(); if (selection || draft) { operation = undefined; return; } - if (!loadFragment()) { const initial = entries.find(e => e.id === 'intro') ?? entries[0]; if (!initial) throw new Error('Empty catalogue'); selectExample(initial); } + if (!loadFragment()) { const initial = entries.find(e => e.id === 'names-values') ?? entries[0]; if (!initial) throw new Error('Empty catalogue'); selectExample(initial); } } catch (error) { initialized = false; loaded = false; finish(error instanceof Error ? error.message : String(error), true); } } setupInspector(); diff --git a/ext/demo-site/src/demo-inputs.ts b/ext/demo-site/src/demo-inputs.ts @@ -2,7 +2,13 @@ // The argument order comes from the published demos beside this site. export function demoInputLabels(id: string, definition: string): readonly string[] { switch (id) { - case 'intro': return definition === 'first' ? ['First value', 'Second value'] : definition === 'identity' ? ['Value'] : []; + case 'functions': return definition === 'add' ? ['Left number', 'Right number'] : ['Number']; + case 'transform-list': return definition === 'double' ? ['Number'] : []; + case 'choices': return ['Number of items']; + case 'language-reference': + if (definition === 'first') return ['First value', 'Second value']; + if (definition === 'firstOr') return ['Fallback', 'List']; + return ['identity', 'addTwo', 'describe', 'keep', 'local', 'viaWhere', 'copy', 'localCopy', 'checked', 'sequenced'].includes(definition) ? ['Value'] : []; case 'microscope': return definition === 'same' ? ['First tree', 'Second tree'] : ['Tree to inspect']; case 'rgb': return ['RGB channels (0–255)']; case 'shipping': return ['Destination', 'Cart subtotal (cents)', 'Package weight (grams)']; diff --git a/ext/demo-site/src/program-catalogue.ts b/ext/demo-site/src/program-catalogue.ts @@ -0,0 +1,23 @@ +import { packageReleases, type Release } from './packages.js'; +import type { CatalogueEntry } from './protocol.js'; +export const exampleGroups = ['Basic Examples', 'Practical Examples', 'Language & Runtime Explorations'] as const; +export function exampleGroup(entry: CatalogueEntry): typeof exampleGroups[number] { + return entry.category ?? (['microscope', 'parser', 'lexer'].includes(entry.id) ? 'Language & Runtime Explorations' : 'Practical Examples'); +} +export function libraryLabel(name: string): string { + if (name === 'tricu-lib-packages-base') return 'base · convenience interface'; + if (name === 'tricu-lib-packages-io') return 'io · convenience interface'; + if (name === 'tricu-compiler') return 'compiler · combined toolchain'; + if (name === 'tricu-source-builder') return 'source-builder · native entrypoint'; + return name.replace(/^tricu-lib-/u, '').replace(/^tricu-/u, ''); +} +// Publication names may be aliases of exactly the same native module. Prefer +// filesystem library publications, while keeping distinct interfaces separate. +// Historical releases remain in the registry and reachable by bundle hash. +export function selectorLibraries(releases: readonly Release[]): Release[] { + const latest = [...new Set(releases.map(r => r.name))].map(name => packageReleases(releases, name)[0]!).filter(r => !r.demo); + latest.sort((a, b) => Number(b.name.startsWith('tricu-lib-')) - Number(a.name.startsWith('tricu-lib-')) || a.name.localeCompare(b.name)); + const manifests = new Set<string>(); + return latest.filter(r => { if (manifests.has(r.manifest)) return false; manifests.add(r.manifest); return true; }) + .sort((a, b) => libraryLabel(a.name).localeCompare(libraryLabel(b.name))); +} diff --git a/ext/demo-site/src/protocol.ts b/ext/demo-site/src/protocol.ts @@ -7,6 +7,7 @@ export type RequestId = number & { readonly __requestId: unique symbol }; export interface CatalogueEntry { id: string; title: string; hash: BundleHash; source: string; sourceSha256: string; export: string; arguments: unknown[]; output: OutputType; mode: 'pure' | 'io'; + category?: 'Basic Examples' | 'Practical Examples' | 'Language & Runtime Explorations'; environment?: string; presets?: Record<string, { arguments: unknown[]; output: OutputType; mode: 'pure' | 'io' }>; } diff --git a/ext/demo-site/style.css b/ext/demo-site/style.css @@ -1,7 +1,7 @@ :root{color-scheme:light;--paper:#f5f2ea;--ink:#202d29;--muted:#626d65;--green:#235a43;--line:#d8dcd0;--panel:#fffef9;--code:#172a24;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(--green);color:white;border-color:var(--green)}:focus-visible{outline:3px solid #b65b28;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:#9b3422}.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} /* One workspace, one output surface. No nested cards or inactive panels. */ -.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:#eaf0e6}.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}.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/1.65 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}.expression-dock{padding:16px 20px;background:#eef2e8;border-top:1px solid var(--line)}#expression-inputs label{font-size:13px}#expression-inputs label span{font-weight:400;color:var(--muted)}#expression-editor{font:16px/1.6 ui-monospace,monospace;min-height:92px;background:var(--panel)}#expression-help{margin:6px 0 0}#draft-run-note{color:#8b581b}.argument-heading{display:flex;justify-content:space-between;align-items:center;gap:12px}.argument-heading h3{margin:0}#argument-fields{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px}.argument-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 10px;margin-top:12px}.argument-row label{grid-column:1/-1}.argument-row textarea{font-size:16px}.argument-row button{font-size:12px;align-self:start} +.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:#eaf0e6}.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}.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}.expression-dock{padding:16px 20px;background:#eef2e8;border-top:1px solid var(--line)}#expression-inputs label{font-size:13px}#expression-inputs label span{font-weight:400;color:var(--muted)}#expression-editor{font:16px/26px ui-monospace,monospace;min-height:92px;background:var(--panel)}#expression-help{margin:6px 0 0}#draft-run-note{color:#8b581b}.argument-heading{display:flex;justify-content:space-between;align-items:center;gap:12px}.argument-heading h3{margin:0}#argument-fields{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px}.argument-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 10px;margin-top:12px}.argument-row label{grid-column:1/-1}.argument-row textarea{font-size:16px}.argument-row button{font-size:12px;align-self:start} /* Docked settings; mobile turns the same inspector into a modal sheet. */ #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} .output-panel{border-top:1px solid var(--line);padding:20px;min-width:0}.output-toolbar{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}.output-toolbar>div{min-width:0}#output-provenance{margin-block:16px 8px}#result{min-height:64px;max-height:520px;overflow:auto;margin:0;padding:16px;background:#f3f4ee;border:1px solid var(--line);border-radius:4px}#result:empty::before{content:"Your output appears here.";color:var(--muted)}#result-stale{border-left:3px solid #b28132;padding-left:10px}.output-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px}#io-console{margin-top:20px}#terminal{max-height:260px;overflow:auto;padding:12px;border-left:3px solid var(--green);background:#eef2e8}#terminal: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} @@ -9,8 +9,9 @@ @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;padding:16px;width:100%;height:360px;min-height:240px;max-height:none}.source-actions,.expression-dock,.output-panel{padding-inline:16px}.output-toolbar{grid-template-columns:repeat(2,minmax(0,1fr))}.output-toolbar select{font-size: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}#argument-fields{grid-template-columns:minmax(0,1fr)}} +@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,.expression-dock,.output-panel{padding-inline:16px}.output-toolbar{grid-template-columns:repeat(2,minmax(0,1fr))}.output-toolbar select{font-size: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}#argument-fields{grid-template-columns:minmax(0,1fr)}} @media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}} +/* Whole-pixel editor line heights keep textarea and mirror layout aligned on long sources. */ /* Native textarea editing, decorative syntax mirror. No contenteditable/HTML input. */ .syntax-editor{position:relative;min-width:0;background:var(--panel);--syntax-ink:var(--ink);--syn-comment:#56695e;--syn-string:#27613d;--syn-escape:#95511e;--syn-number:#27578c;--syn-keyword:#6c367c;--syn-definition:#754300;--syn-tree:#006b60;--syn-contract:#934824;--syn-invalid:#ae3026;--syn-match:#d4e5c4} .source-panel{--syntax-ink:#e3edde;--syn-comment:#9cafa4;--syn-string:#c0d9a3;--syn-escape:#f4bc8d;--syn-number:#9ecff5;--syn-keyword:#d6b5ef;--syn-definition:#ffd08a;--syn-tree:#73dbc0;--syn-contract:#f4bc8d;--syn-invalid:#ffb2a6;--syn-match:#435b43} diff --git a/ext/demo-site/test/asset-cache-probe.mjs b/ext/demo-site/test/asset-cache-probe.mjs @@ -18,7 +18,7 @@ export async function run(control,backend='haskell-wasm'){ const evaluate=async()=>{input('expression-editor','42');change('output-type','number');change('mode','pure');el('run').click();await status('Finished');assert(el('result').textContent==='42','Evaluation works');}; await evaluate(); // Warm the compiler and expression path too. const warm=code(await requests());assert(warm.length>=5,'Network observed the initial dependency graph'); - for(const name of ['rgb','microscope','intro']){ + for(const name of ['rgb','microscope','names-values']){ change('program-picker','demo:'+name);await status('Ready');await evaluate(); } input('source-editor','!export answer\nanswer = 42');await status('Compiled'); diff --git a/ext/demo-site/test/backend-browser.mjs b/ext/demo-site/test/backend-browser.mjs @@ -53,7 +53,7 @@ try { return result.result.value; }; for(let i=0;!await evaluate(`location.href===${JSON.stringify(url)} && document.readyState==='complete'`);i++){if(i>400)throw Error('Page did not load');await new Promise(r=>setTimeout(r,25));} - const report=await evaluate(`import(${JSON.stringify(process.env.TEST_IMPORT_PICKER?'/import-picker-probe.mjs':process.env.TEST_COLD_LOAD?'/cold-load-probe.mjs':process.env.TEST_ASSET_CACHE?'/asset-cache-probe.mjs':process.env.TEST_LOCAL_MODULES?'/local-module-probe.mjs':process.env.TEST_MODULES?'/module-probe.mjs':process.env.TEST_IDE?'/ide-probe.mjs':process.env.TEST_LIVE_UX?'/live-ux-probe.mjs':process.env.TEST_BENCHMARK?'/backend-benchmark.mjs':'/backend-probe.mjs')}).then(m=>m.run(${JSON.stringify(control)},${JSON.stringify(process.env.TEST_BACKEND||'haskell-wasm')},${!!process.env.BENCHMARK_PSS}))`); + const report=await evaluate(`import(${JSON.stringify(process.env.TEST_BASIC_EXAMPLES?'/basic-examples-probe.mjs':process.env.TEST_IMPORT_PICKER?'/import-picker-probe.mjs':process.env.TEST_COLD_LOAD?'/cold-load-probe.mjs':process.env.TEST_ASSET_CACHE?'/asset-cache-probe.mjs':process.env.TEST_LOCAL_MODULES?'/local-module-probe.mjs':process.env.TEST_MODULES?'/module-probe.mjs':process.env.TEST_IDE?'/ide-probe.mjs':process.env.TEST_LIVE_UX?'/live-ux-probe.mjs':process.env.TEST_BENCHMARK?'/backend-benchmark.mjs':'/backend-probe.mjs')}).then(m=>m.run(${JSON.stringify(control)},${JSON.stringify(process.env.TEST_BACKEND||'haskell-wasm')},${!!process.env.BENCHMARK_PSS}))`); assert.deepEqual(exceptions,[]); if(process.env.TEST_LIVE_UX||process.env.TEST_IDE){ for(const width of [320,390,1440]){ diff --git a/ext/demo-site/test/backend-probe.mjs b/ext/demo-site/test/backend-probe.mjs @@ -24,13 +24,13 @@ export async function run(control, selectedBackend='haskell-wasm') { document.getElementById('use-guided-inputs').checked=true;document.getElementById('use-guided-inputs').dispatchEvent(new Event('change')); await wait(status('Ready')); await click('run');await wait(status('Finished')); - assert.equal(await evaluate("document.getElementById('result').textContent"),'42'); + assert.equal(await evaluate("document.getElementById('result').textContent"),'Hello, world!'); assert.equal((await requestLog()).some(x=>x.endsWith('/runtime/haskell/reactor.wasm')),true,'Default actually loads Haskell'); await fault({runtime:true}); await selectBackend();await wait(status('Stopped')); assert.match(await evaluate("document.getElementById('result').textContent"),/WASM fetch failed|HTTP|fetch/i); assert.equal(await evaluate("document.getElementById('run').disabled"),true,'runtime failure must not fall back to JS'); - await fault({runtime:false});await select('intro'); + await fault({runtime:false});await select('language-reference'); assert.equal((await requestLog()).some(x=>x.endsWith('/data/compiler.dag')||x.endsWith('/hash/'+compilerHash)),false,'packages do not load a compiler kit'); await click('run');await wait(status('Finished')); assert.equal(await evaluate("document.getElementById('result').textContent"),'42'); @@ -47,7 +47,7 @@ export async function run(control, selectedBackend='haskell-wasm') { await evaluate("document.getElementById('output-type').value='dag';document.getElementById('output-type').dispatchEvent(new Event('change'))"); await click('run');await wait("!document.getElementById('copy-output').disabled"); assert.match(await evaluate("document.getElementById('result').textContent"),/tree/); - await evaluate("document.getElementById('output-target').value='result';document.getElementById('output-target').dispatchEvent(new Event('change'))");await select('intro'); + await evaluate("document.getElementById('output-target').value='result';document.getElementById('output-target').dispatchEvent(new Event('change'))");await select('language-reference'); await evaluate("document.getElementById('source-editor').value=document.getElementById('source-editor').value.replace('first 42 7','first 43 7');document.getElementById('source-editor').dispatchEvent(new Event('input'))"); await click('compile');await wait(status('Compiled')); assert.equal((await requestLog()).some(x=>x.endsWith('/data/compiler.dag')||x.endsWith('/hash/'+compilerHash)),true); diff --git a/ext/demo-site/test/backend-server.mjs b/ext/demo-site/test/backend-server.mjs @@ -41,6 +41,7 @@ export async function startSiteServer(directory,autoRun=false) { if(path==='/syntax-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./syntax-probe.mjs',import.meta.url)));return;} if(path==='/local-module-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./local-module-probe.mjs',import.meta.url)));return;} if(path==='/module-goldens.json'){res.setHeader('Content-Type','application/json');res.end(await readFile(new URL('./module-goldens.json',import.meta.url)));return;} + if(path==='/basic-examples-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./basic-examples-probe.mjs',import.meta.url)));return;} if(path==='/import-picker-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./import-picker-probe.mjs',import.meta.url)));return;} if(path==='/module-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./module-probe.mjs',import.meta.url)));return;} if(path==='/ide-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./ide-probe.mjs',import.meta.url)));return;} @@ -79,7 +80,7 @@ export async function startSiteServer(directory,autoRun=false) { if(moduleCorrupt&&path==='/_arboricx/bundle/hash/'+moduleCorrupt){bytes=Buffer.from(bytes);bytes[0]^=1;} if(process.env.TEST_COLD_LOAD&&path==='/nested/site/index.html')bytes=Buffer.from(bytes.toString().replace('<head>',`<head><script>new MutationObserver((_,observer)=>{if(document.getElementById('status')?.textContent==='Ready'){globalThis.__readyMs=performance.now();observer.disconnect();}}).observe(document.documentElement,{childList:true,subtree:true,characterData:true});</script>`)); if(autoRun&&path==='/nested/site/index.html'&&!new URL(req.url,'http://localhost').searchParams.has('noProbe'))bytes=Buffer.from(bytes.toString()+`<script type="module"> - try{const m=await import(${JSON.stringify(process.env.TEST_IMPORT_PICKER?'/import-picker-probe.mjs':process.env.TEST_COLD_LOAD?'/cold-load-probe.mjs':process.env.TEST_ASSET_CACHE?'/asset-cache-probe.mjs':process.env.TEST_LOCAL_MODULES?'/local-module-probe.mjs':process.env.TEST_MODULES?'/module-probe.mjs':process.env.TEST_IDE?'/ide-probe.mjs':process.env.TEST_LIVE_UX?'/live-ux-probe.mjs':process.env.TEST_BENCHMARK?'/backend-benchmark.mjs':'/backend-probe.mjs')});const report=await m.run(${JSON.stringify(control)},${JSON.stringify(process.env.TEST_BACKEND||'haskell-wasm')},${!!process.env.BENCHMARK_PSS});await fetch(${JSON.stringify(control+'/report')},{method:'POST',body:JSON.stringify({ok:true,report})});} + try{const m=await import(${JSON.stringify(process.env.TEST_BASIC_EXAMPLES?'/basic-examples-probe.mjs':process.env.TEST_IMPORT_PICKER?'/import-picker-probe.mjs':process.env.TEST_COLD_LOAD?'/cold-load-probe.mjs':process.env.TEST_ASSET_CACHE?'/asset-cache-probe.mjs':process.env.TEST_LOCAL_MODULES?'/local-module-probe.mjs':process.env.TEST_MODULES?'/module-probe.mjs':process.env.TEST_IDE?'/ide-probe.mjs':process.env.TEST_LIVE_UX?'/live-ux-probe.mjs':process.env.TEST_BENCHMARK?'/backend-benchmark.mjs':'/backend-probe.mjs')});const report=await m.run(${JSON.stringify(control)},${JSON.stringify(process.env.TEST_BACKEND||'haskell-wasm')},${!!process.env.BENCHMARK_PSS});await fetch(${JSON.stringify(control+'/report')},{method:'POST',body:JSON.stringify({ok:true,report})});} catch(e){await fetch(${JSON.stringify(control+'/report')},{method:'POST',body:JSON.stringify({ok:false,error:String(e)+'\\n'+(e.stack||'')})});} </script>`); if(process.env.TEST_ASSET_CACHE && ['/nested/site/index.html','/nested/site/assets.json'].includes(path))res.setHeader('Cache-Control','no-cache'); diff --git a/ext/demo-site/test/basic-examples-probe.mjs b/ext/demo-site/test/basic-examples-probe.mjs @@ -0,0 +1,54 @@ +const el=id=>document.getElementById(id),delay=ms=>new Promise(r=>setTimeout(r,ms)); +const assert=(ok,message)=>{if(!ok)throw Error(message);}; +const input=(id,value)=>{el(id).value=value;el(id).dispatchEvent(new Event('input'));}; +const change=(id,value)=>{el(id).value=value;el(id).dispatchEvent(new Event('change'));}; +async function status(want){for(let i=0;i<4500;i++){if(el('status').textContent===want)return;await delay(20);}throw Error(want+': '+el('status').textContent+' / '+el('compile-diagnostic').textContent+' / '+el('result').textContent);} +export async function run(control,backend='haskell-wasm'){ + await status('Ready'); + const groups=[...el('program-picker').querySelectorAll('optgroup')]; + assert(groups.map(g=>g.label).join('|')==='Basic Examples|Practical Examples|Language & Runtime Explorations|Libraries','Purposeful selector groups'); + const basics=['names-values','arithmetic','functions','lists','transform-list','choices']; + assert([...groups[0].children].map(o=>o.value).join('|')===basics.map(n=>'demo:'+n).join('|'),'Basic progression'); + 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').value==='greeting'&&el('output-type').value==='string','Beginner arrival is ready to evaluate'); + assert(el('result').textContent===''&&el('terminal').textContent==='','Arrival waits for Evaluate'); + change('runtime-backend',backend);await status('Ready'); + const base=new URL('../',document.querySelector('script[type=module][src]').src); + const releases=(await(await fetch(new URL('packages.json',base))).json()).releases; + const visible=[...groups[3].children],manifests=visible.map(o=>releases.find(r=>r.name===o.value.slice(8))?.manifest); + assert(new Set(manifests).size===manifests.length,'One selector row per module identity'); + assert(visible.every(o=>!o.textContent.startsWith('tricu-')),'Friendly library labels'); + assert(visible.some(o=>o.value==='package:tricu-lib-native-math')&&!visible.some(o=>o.value==='package:tricu-math'),'Complete library publication preferred'); + const select=async name=>{change('program-picker','demo:'+name);await status('Ready');}; + const evaluate=async(expression,output,expected)=>{input('expression-editor',expression);change('output-type',output);el('run').click();await status('Finished');assert(el('result').textContent===expected,expression+' expected '+expected+' got '+el('result').textContent);}; + 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.']]; + for(let i=0;i<basics.length;i++){ + await select(basics[i]);assert(el('expression-editor').value===defaults[i][0]&&el('output-type').value===defaults[i][1],'Default expression/settings '+basics[i]); + el('run').click();await status('Finished');assert(el('result').textContent===defaults[i][2],'Default result '+basics[i]); + const original=el('source-editor').value; + 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===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(el('source-editor').value===original,'Restore '+basics[i]); + } + await select('language-reference'); + // Each preset goes through the actual published page (before editing), so the + // examples must work using the public interface rather than hidden imports. + const catalogue=await(await fetch(new URL('catalogue.json',base))).json(); + const reference=catalogue.find(d=>d.id==='language-reference'); + for(const name of Object.keys(reference.presets)){ + change('export',name);el('run').click();await status('Finished');assert(el('result').textContent.length>0,'Reference preset '+name); + } + await evaluate('describe (t t)','string','true');await evaluate('describe "hi"','string','greeting');await evaluate('firstOr 99 []','number','99'); + input('source-editor',reference.source+'\n-- my reference notes');await status('Compiled');await evaluate('sequenced 9','number','9'); + // A retired example's immutable publication still loads through its old share link. + const old=releases.find(r=>r.name==='intro'&&r.version==='0.2.0'); + location.hash=new URLSearchParams({bundle:old.hash,demo:'intro',export:'answer'}).toString();await delay(50);await status('Ready'); + assert(el('program-picker').value==='hash','Retired link remains a direct bundle selection');await evaluate('answer','number','42'); + await select('names-values'); + return {userAgent:navigator.userAgent,checks:['beginner default; grouped selector; identity-based library deduplication; all six lesson defaults and exercises; restoration; all reference presets before editing; editable reference; retained historical link'],backend}; +} diff --git a/ext/demo-site/test/evaluation-layout-probe.mjs b/ext/demo-site/test/evaluation-layout-probe.mjs @@ -39,5 +39,5 @@ export async function evaluationLayout(){ window.scrollTo({top:0,behavior:'instant'});await frame();input('line-input','hello');el('input-form').requestSubmit();await wait('Finished');assert(scrollY===0,'IO completion does not scroll again'); // Idle compilation has no reveal authority. change('mode','pure');window.scrollTo({top:0,behavior:'instant'});await frame();input('source-editor','answer = t');await wait('Compiled');assert(scrollY===0,'Idle compilation leaves the viewport alone'); - change('program-picker','demo:intro');await wait('Ready'); + change('program-picker','demo:language-reference');await wait('Ready'); } diff --git a/ext/demo-site/test/ide-probe.mjs b/ext/demo-site/test/ide-probe.mjs @@ -12,7 +12,7 @@ export async function run(control,backend='haskell-wasm'){ await syntaxProbe(); assert([...el('runtime-backend').options].map(o=>o.value).join(',')==='haskell-wasm,zig-wasm','Only Haskell and Zig are production runtimes'); 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 are not shipped: '+path); - assert(el('program-picker').value==='demo:intro','Start Here loads on arrival'); + assert(el('program-picker').value==='demo:names-values','Names and values loads on arrival'); assert(!document.querySelector('[role=tab],#graph-result,#export-definition'),'No tabs or graph-JSON surface'); assert(!el('result').textContent&&!el('terminal').textContent,'Arrival does not execute'); assert(document.querySelector('footer a[href="https://eversole.co"]').textContent==='James Eversole','Copyright author link'); @@ -23,6 +23,7 @@ export async function run(control,backend='haskell-wasm'){ const requests=async()=>await(await fetch(control+'/requests')).json(); const before=(await requests()).filter(x=>x.endsWith('/compiler.json')||x.endsWith('/data/compiler.dag')).length; assert(before===0,'Arrival does not load compiler assets'); + change('program-picker','demo:language-reference');await status('Ready'); change('output-target','definition');change('export','leaf'); assert(el('run').textContent==='Show definition'&&el('mode').disabled,'Stored target is visibly distinct from execution'); for(const [format,want] of [['tricu','t'],['triangle','△'],['dag','tree △\ntree\n'],['ternary','0']]){ @@ -71,9 +72,9 @@ export async function run(control,backend='haskell-wasm'){ await evaluate('identity 53','number');assert(el('result').textContent==='53','Retry authenticates the compiler'); input('bundle-hash','not-a-hash');el('program-hash').requestSubmit();await status('Stopped'); assert(el('run').disabled&&el('copy-output').disabled,'Invalid replacement never falls back to an earlier package/output'); - change('program-picker','demo:intro');await status('Ready'); + change('program-picker','demo:language-reference');await status('Ready'); const live=await(await import('/live-ux-probe.mjs')).run(control,backend,true); - change('program-picker','demo:intro');await status('Ready'); + change('program-picker','demo:language-reference');await status('Ready'); await(await import('/evaluation-layout-probe.mjs')).evaluationLayout(); return {userAgent:navigator.userAgent,checks:[...live.checks,'single-page arrival/inspector; stored vs applied outputs; t/△/DAG/ternary; IO result; complete copy/download; expansion refusal; immutable bundle; pin failure/retry']}; } diff --git a/ext/demo-site/test/live-ux-probe.mjs b/ext/demo-site/test/live-ux-probe.mjs @@ -21,7 +21,8 @@ export async function run(control,backend='haskell-wasm',alreadyUsed=false){ const {Graph}=(await import(new URL('dist/probe-api.js',base))).graph; const {view,readList}=(await import(new URL('dist/probe-api.js',base))).codecs; const {printLiteral}=(await import(new URL('dist/probe-api.js',base))).literalInput; - for(const [name,demo] of Object.entries(fixtures.demos)){ + const active=new Set([...el('program-picker').options].map(o=>o.value)); + for(const [name,demo] of Object.entries(fixtures.demos).filter(([name])=>active.has('demo:'+name))){ change('program-picker','demo:'+name);await status('Ready'); const c=demo.haskellCases[0];change('export',c.export); const asSource=ternary=>{let at=0;const read=()=>{const tag=ternary[at++];return tag==='0'?'t':tag==='1'?`(t ${read()})`:`(t ${read()} ${read()})`;};return read();}; @@ -40,7 +41,7 @@ export async function run(control,backend='haskell-wasm',alreadyUsed=false){ assert(el('result').textContent===(typeof expected.value==='string'?expected.value:JSON.stringify(expected.value,null,2)),'Complete expression golden: '+name+' '+el('result').textContent); assert(el('terminal').textContent===c.terminal,'Complete terminal golden: '+name); } - change('program-picker','demo:intro');await status('Ready'); + change('program-picker','demo:language-reference');await status('Ready'); await evaluate('identity (first 42 7)','42'); await evaluate('let\n local x = identity x\n in local 19','19'); const exports=[...el('export').options].map(x=>x.value).join(','); @@ -52,7 +53,7 @@ export async function run(control,backend='haskell-wasm',alreadyUsed=false){ assert([...el('export').options].map(x=>x.value).join(',')===exports,'Expressions never publish temporary names'); // No editable source required: bind exact published library exports to trees. assert(el('settings-panel').hidden,'Package loading must not require opening Settings'); - change('program-picker','package:tricu-base');await status('Ready'); + change('program-picker','package:tricu-lib-packages-base');await status('Ready'); assert(!el('run').disabled&&el('settings-panel').hidden,'Selecting a package makes it usable without a hidden Load action'); assert(el('result').textContent===''&&el('terminal').textContent==='','Package selection only loads; it never evaluates'); assert(el('source-editor').hidden,'Library has no editable source'); @@ -60,7 +61,7 @@ export async function run(control,backend='haskell-wasm',alreadyUsed=false){ await evaluate('append "hello " "λ🙂"','hello λ🙂','string'); await evaluate('showNumber 30','30','string'); // Source revision safety, deterministic late completion, and edit coalescing. - change('program-picker','demo:intro');await status('Ready'); + change('program-picker','demo:language-reference');await status('Ready'); await evaluate('answer','42'); const NativeWorker=globalThis.Worker,held=[],submitted=[];let hold=true; globalThis.Worker=class extends NativeWorker { @@ -101,7 +102,7 @@ export async function run(control,backend='haskell-wasm',alreadyUsed=false){ input('source-editor','answer = 49');await delay(900); assert(el('status').textContent==='Changes pending','IME input is not compiled mid-composition'); el('source-editor').dispatchEvent(new CompositionEvent('compositionend'));await status('Compiled');await evaluate('answer','49'); - input('source-editor','answer = 50');change('program-picker','demo:intro');await status('Ready');await delay(900); + input('source-editor','answer = 50');change('program-picker','demo:language-reference');await status('Ready');await delay(900); assert(el('status').textContent==='Ready','Changing package cancels queued source compilation'); // An IO expression is never executed just by editing/compiling it. document.querySelector('[data-compiler-example=input]').click();await status('Compiled'); diff --git a/ext/demo-site/test/module-probe.mjs b/ext/demo-site/test/module-probe.mjs @@ -139,5 +139,5 @@ export async function run(control,backend='haskell-wasm'){ input('expression-editor',expression);change('output-type','record');el('run').click();await status('Finished'); equal(JSON.parse(el('result').textContent)[field],original,'Restored '+id+' behavior'); } - return {userAgent:navigator.userAgent,checks:['all ten editable demos match native goldens; parser/lexer edit and restore; native headers; qualified/open imports; full reexports; retained private/import scope; collision/diagnostic controls; transport and manifest faults; no capabilities; repeated replacement/GC; actual page Stop/recompile']}; + return {userAgent:navigator.userAgent,checks:['all sixteen editable demos match native goldens; parser/lexer edit and restore; native headers; qualified/open imports; full reexports; retained private/import scope; collision/diagnostic controls; transport and manifest faults; no capabilities; repeated replacement/GC; actual page Stop/recompile']}; } diff --git a/ext/demo-site/test/packages.test.mjs b/ext/demo-site/test/packages.test.mjs @@ -152,7 +152,9 @@ test('release choices are newest-first, numeric, independent of publication orde test('every catalogue demo is editable, source-authenticated and explicitly imports indexed manifests',async()=>{ const catalogue=JSON.parse(await read('catalogue.json')),index=packageIndex(JSON.parse(await read('packages.json'))); - assert.equal(catalogue.length,10); + assert.equal(catalogue.length,16); + assert.equal(catalogue[0].id,'names-values'); + assert.ok(!catalogue.some(e=>e.id==='intro')); for(const demo of catalogue){ assert.equal(demo.environment,'empty',demo.id); assert.equal(demo.source,(await read('demos/'+demo.id+'.tri')).toString()); diff --git a/ext/demo-site/test/program-catalogue.test.mjs b/ext/demo-site/test/program-catalogue.test.mjs @@ -0,0 +1,30 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {readFile} from 'node:fs/promises'; +import {selectorLibraries,libraryLabel,exampleGroup,exampleGroups} from '../dist/program-catalogue.js'; +import {packageIndex,packageReleases} from '../dist/packages.js'; +const read=name=>readFile(new URL('../'+name,import.meta.url),'utf8').then(JSON.parse); +test('selector merges identical native interfaces while preserving publications',async()=>{ + const releases=packageIndex(await read('packages.json')),before=JSON.stringify(releases),shown=selectorLibraries(releases); + assert.equal(new Set(shown.map(r=>r.manifest)).size,shown.length); + assert.equal(JSON.stringify(releases),before); + assert(shown.some(r=>r.name==='tricu-lib-native-math'));assert(!shown.some(r=>r.name==='tricu-math')); + assert(shown.some(r=>r.name==='tricu-lib-packages-base'));assert(!shown.some(r=>r.name==='tricu-base')); + assert(shown.some(r=>r.name==='tricu-compiler'));assert(shown.some(r=>r.name==='tricu-source-builder')); + for(const name of new Set(releases.filter(r=>!r.demo).map(r=>r.name))){ + const latest=packageReleases(releases,name)[0];assert(shown.some(r=>r.manifest===latest.manifest),name); + } + assert(shown.every(r=>!libraryLabel(r.name).startsWith('tricu-'))); + assert(releases.some(r=>r.demo==='intro'),'Retired publications remain available'); +}); +test('beginner progression and reference are separated from practical demos',async()=>{ + const catalogue=await read('catalogue.json'); + assert.deepEqual(exampleGroups,['Basic Examples','Practical Examples','Language & Runtime Explorations']); + assert.deepEqual(catalogue.filter(e=>exampleGroup(e)==='Basic Examples').map(e=>e.id),['names-values','arithmetic','functions','lists','transform-list','choices']); + 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(Object.keys(catalogue.find(e=>e.id==='language-reference').presets).length,16); +}); diff --git a/ext/demo-site/test/site.test.mjs b/ext/demo-site/test/site.test.mjs @@ -33,7 +33,7 @@ test('inspector supports mobile modality, escape, focus return and focus contain assert.match(html,/aria-controls="settings-panel" aria-expanded="false"/); for(const token of ['aria-modal','aria-expanded','element.inert','Escape','Tab','toggle.focus()','getClientRects'])assert.ok(controller.includes(token),token); const css=await read('style.css');assert.match(css,/\.workspace\{width:100%;max-width:none;margin:0;border-left:0;border-right:0;border-radius:0/); - assert.match(css,/#source-editor\{font-size:16px;padding:16px;width:100%/); + assert.match(css,/#source-editor\{font-size:16px;line-height:27px;padding:16px;width:100%/); assert.match(css,/prefers-reduced-motion/); }); test('ecosystem remains with copyright and footer feedback',async()=>{ diff --git a/ext/demo-site/test/syntax-probe.mjs b/ext/demo-site/test/syntax-probe.mjs @@ -6,7 +6,7 @@ export async function syntaxProbe(){ assert(source.closest('.syntax-editor').querySelector('.syntax-definition'),'Loaded source declarations are highlighted'); const sourceMirror=source.closest('.syntax-editor').querySelector('pre'); await new Promise(r=>requestAnimationFrame(()=>requestAnimationFrame(r))); - assert(Math.abs(sourceMirror.scrollHeight-source.scrollHeight)<=2,'Source mirror wrapping matches native editor at this viewport'); + assert(Math.abs(sourceMirror.scrollHeight-source.scrollHeight)<=2,'Source mirror wrapping matches native editor: '+JSON.stringify({viewport:innerWidth,input:source.scrollHeight,mirror:sourceMirror.scrollHeight,width:source.clientWidth,mirrorWidth:sourceMirror.style.width,lineHeight:getComputedStyle(source).lineHeight})); assert(document.querySelector('#source .syntax-definition'),'Read-only source gets the same tokens'); assert(wrapper.querySelector('[aria-hidden=true]')&&input.tagName==='TEXTAREA','Decorative mirror leaves native accessible textarea intact'); const saved=input.value; diff --git a/ext/demo-site/tutorials.py b/ext/demo-site/tutorials.py @@ -0,0 +1,70 @@ +"""Beginner/reference catalogue settings and independent native expectations.""" +def literal(value): + if isinstance(value, dict): return value + if isinstance(value, bool): return dict(type='bool', value=value) + if isinstance(value, int): return dict(type='number', value=str(value)) + if isinstance(value, str): return dict(type='string', value=value) + return dict(type='list', value=[literal(x) for x in value]) + + +def tree(value): return dict(type='tree', value=value) + + +def examples(): + # First case is the initial expression. Other cases double as definition + # 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')]), + 'arithmetic': ('Doing arithmetic', 'Basic Examples', [ + ('total', [], 12, 'number'), ('product', [], 35, 'number'), ('grouped', [], 24, 'number')]), + 'functions': ('Your first function', 'Basic Examples', [ + ('double', [6], 12, 'number'), ('double', [9], 18, 'number'), ('add', [4, 7], 11, 'number')]), + 'lists': ('Working with lists', 'Basic Examples', [ + ('count', [], 3, 'number'), ('total', [], 12, 'number')]), + 'transform-list': ('Transforming a list', 'Basic Examples', [ + ('total', [], 24, 'number'), ('double', [5], 10, 'number')]), + 'choices': ('Making a choice', 'Basic Examples', [ + ('message', [0], 'Your basket is empty.', 'string'), + ('message', [1], 'You have one item.', 'string'), + ('message', [4], 'You have several items.', 'string')]), + 'language-reference': ('Language reference', 'Language & Runtime Explorations', [ + ('answer', [], 42, 'number'), ('identity', [7], 7, 'number'), ('identity', ['hello'], 'hello', 'string'), + ('first', [7, 9], 7, 'number'), ('leaf', [], tree('0'), 'tricu'), + ('shapes', [], [tree('0'), tree('10'), tree('200')], 'tricu'), + ('literals', [], ['hello', 42, [1, 2, 3]], 'tricu'), + ('addTwo', [5], 7, 'number'), + ('describe', [7], 'seven', 'string'), ('describe', ['hi'], 'greeting', 'string'), + ('describe', [True], 'true', 'string'), ('describe', [[4, 5]], 'two items', 'string'), + ('describe', [[1, 2, 3]], 'nonempty list', 'string'), ('describe', [[]], 'empty list', 'string'), + ('describe', [tree('110')], 'other', 'string'), + ('keep', [tree('200')], tree('200'), 'tricu'), ('keep', [tree('10')], tree('0'), 'tricu'), + ('local', [6], 12, 'number'), ('viaWhere', [6], 7, 'number'), + ('copy', [[1, 2, 3]], [1, 2, 3], 'bytes'), ('copy', [[]], [], 'bytes'), ('copy', [tree('10')], [], 'bytes'), + ('firstOr', [99, [5]], 5, 'number'), ('firstOr', [99, []], 99, 'number'), + ('localCopy', [[4, 5]], [4, 5], 'bytes'), ('localCopy', [[]], [], 'bytes'), ('localCopy', [tree('10')], [], 'bytes'), + ('checked', [42], 42, 'number'), ('sequenced', [9], 9, 'number'), + ]), + } + + +def encoded(value): return value if isinstance(value, dict) else literal(value) + + +def settings(): + result = {} + for name, (title, category, cases) in examples().items(): + presets = {} + for export, arguments, _, output in cases: + presets.setdefault(export, dict(arguments=[encoded(a) for a in arguments], output=output, mode='pure')) + 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 + + +def cases(): + return {name: [(export, [encoded(a) for a in arguments], encoded(expected)) + for export, arguments, expected, _ in rows] + for name, (_, _, rows) in examples().items()}