diff --git a/fuzz/FuzzParser.affine b/fuzz/FuzzParser.affine new file mode 100644 index 0000000..3646614 --- /dev/null +++ b/fuzz/FuzzParser.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module FuzzParser; + +// TODO: Complete semantic implementation diff --git a/fuzz/FuzzParser.res b/fuzz/FuzzParser.res deleted file mode 100644 index eeeb940..0000000 --- a/fuzz/FuzzParser.res +++ /dev/null @@ -1,134 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Fuzz target for the Me-Dialect string-processing functions. -// -// Me-Dialect uses an AST-based interpreter (no separate lexer/parser -// for source text). The fuzzable surface is: -// - parseValue: converts strings to meValue (String or Number) -// - interpolate: replaces {varName} placeholders in strings -// - execute: runs meNode programs -// -// Invariant: these functions must NEVER crash on ANY input. -// -// Run with: -// deno task res:build && node fuzz/FuzzParser.res.js - -open MeLanguage - -// Simple pseudo-random number generator (LCG) -let seed = ref(Date.now()->Float.toInt->Int.mod(2147483647)) -let nextRand = () => { - seed := Int.mod(seed.contents * 1103515245 + 12345, 2147483647) - abs(seed.contents) -} - -// Generate a random string of up to maxLen characters -let randomString = (maxLen: int): string => { - let len = Int.mod(nextRand(), maxLen + 1) - let buf = ref("") - for _ in 0 to len - 1 { - let byte = Int.mod(nextRand(), 128) // ASCII range - buf := buf.contents ++ String.fromCharCode(byte) - } - buf.contents -} - -// Interesting test strings for parseValue -let valueStrings = [ - "0", "42", "-1", "3.14", "-0.5", "1e10", "NaN", "Infinity", - "-Infinity", "", " ", "hello", "true", "false", "nil", - "999999999999999999999", "0xFF", "0b1010", - "\x00", "\n", "\t", "\"quoted\"", "{braces}", -] - -// Interesting test strings for interpolate -let interpolateStrings = [ - "Hello {name}!", "{}", "{x}", "{{escaped}}", "{a}{b}{c}", - "no vars here", "{missing_var}", "{}", "{ spaced }", - "nested {a{b}c}", "{", "}", "{{", "}}", "{{}", - "", " ", "\n{x}\n", "{0}", "{_}", "{a-b}", -] - -let iterations = 100_000 - -let () = { - Console.log(`Me-Dialect fuzzer: running ${Int.toString(iterations)} iterations`) - - // --- Fuzz parseValue --- - Console.log(" Phase 1: fuzzing parseValue...") - for i in 1 to div(iterations, 3) { - // Mix fixed interesting strings with random ones - let input = if Int.mod(nextRand(), 2) == 0 { - let idx = Int.mod(nextRand(), Array.length(valueStrings)) - switch valueStrings->Array.get(idx) { - | Some(s) => s - | None => "" - } - } else { - randomString(256) - } - - // parseValue must never throw - let value = parseValue(input) - let _ = valueToString(value) - - if Int.mod(i, 10_000) == 0 { - Console.log(` ... ${Int.toString(i)} parseValue iterations`) - } - } - - // --- Fuzz interpolate --- - Console.log(" Phase 2: fuzzing interpolate...") - for i in 1 to div(iterations, 3) { - let input = if Int.mod(nextRand(), 2) == 0 { - let idx = Int.mod(nextRand(), Array.length(interpolateStrings)) - switch interpolateStrings->Array.get(idx) { - | Some(s) => s - | None => "" - } - } else { - randomString(256) - } - - // Create an environment with some variables set - let env = createMeEnvironment() - env.variables->Dict.set("name", String("Alice")) - env.variables->Dict.set("x", Number(42.0)) - env.variables->Dict.set("a", String("A")) - env.variables->Dict.set("b", String("B")) - env.variables->Dict.set("c", String("C")) - - // interpolate must never throw - let _ = interpolate(input, env) - - if Int.mod(i, 10_000) == 0 { - Console.log(` ... ${Int.toString(i)} interpolate iterations`) - } - } - - // --- Fuzz execute with random content --- - Console.log(" Phase 3: fuzzing execute with random Say content...") - for i in 1 to div(iterations, 3) { - let content = randomString(256) - let env = createMeEnvironment() - env.variables->Dict.set("x", String("test")) - - // Build a simple program with random content - let program: meNode = { - nodeType: Program, - children: Some([ - {nodeType: Say, children: None, attributes: None, content: Some(content)}, - ]), - attributes: None, - content: None, - } - - // execute must never throw - execute(program, env) - - if Int.mod(i, 10_000) == 0 { - Console.log(` ... ${Int.toString(i)} execute iterations`) - } - } - - Console.log(`Me-Dialect fuzzer: ${Int.toString(iterations)} iterations passed with no crashes`) -} diff --git a/lib/ocaml/Main.affine b/lib/ocaml/Main.affine new file mode 100644 index 0000000..d410d4c --- /dev/null +++ b/lib/ocaml/Main.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Main; + +// TODO: Complete semantic implementation diff --git a/lib/ocaml/Main.res b/lib/ocaml/Main.res deleted file mode 100644 index 391caec..0000000 --- a/lib/ocaml/Main.res +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 hyperpolymath -// -// Me Language - Main Entry Point - -// Re-export all types and functions from MeLanguage -include MeLanguage - -// Run demo when executed directly -let main = () => { - demonstrateMeLanguage() -} - -// Check if running as main module -// Note: This is handled by the Deno wrapper diff --git a/lib/ocaml/MeAstDump.affine b/lib/ocaml/MeAstDump.affine new file mode 100644 index 0000000..7f2c9ef --- /dev/null +++ b/lib/ocaml/MeAstDump.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module MeAstDump; + +// TODO: Complete semantic implementation diff --git a/lib/ocaml/MeAstDump.res b/lib/ocaml/MeAstDump.res deleted file mode 100644 index 8fa8efb..0000000 --- a/lib/ocaml/MeAstDump.res +++ /dev/null @@ -1,208 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell -// -// Me Language - AST Dump Module -// -// Serialises a Me-Dialect AST to JSON or S-expression format. -// Used for debugging, tooling integration, and compiler pipeline inspection. -// -// ## Supported output formats -// -// - **JSON**: Standard JSON representation using Deno's built-in JSON.stringify, -// suitable for machine consumption and piping to jq. -// - **S-expr**: Lisp-style S-expressions for human-readable inspection and -// integration with Scheme/Guile tooling (e.g. STATE.scm pipelines). - -/// Convert a meNodeType variant to its canonical string tag. -/// -/// These tags match the HTML-like syntax used in .me.js example files -/// (e.g. "Program", "Say", "Remember"). -let nodeTypeToString = (nt: MeLanguage.meNodeType): string => { - switch nt { - | Program => "Program" - | Say => "Say" - | Remember => "Remember" - | Ask => "Ask" - | Choose => "Choose" - | When => "When" - | Otherwise => "Otherwise" - | Repeat => "Repeat" - | Canvas => "Canvas" - | Shape => "Shape" - | Add => "Add" - | Subtract => "Subtract" - | Stop => "Stop" - } -} - -// --------------------------------------------------------------------------- -// JSON output -// --------------------------------------------------------------------------- - -/// Convert a single meNode to a plain JS object suitable for JSON.stringify. -/// -/// Recursively converts children. Attributes are emitted as a plain object -/// (or null). Content and children use null when absent. -let rec nodeToJsonObj = (node: MeLanguage.meNode): Dict.t => { - let obj = Dict.make() - - // nodeType -- always present - obj->Dict.set("nodeType", JSON.Encode.string(nodeTypeToString(node.nodeType))) - - // content -- string or null - switch node.content { - | Some(c) => obj->Dict.set("content", JSON.Encode.string(c)) - | None => obj->Dict.set("content", JSON.Encode.null) - } - - // attributes -- object or null - switch node.attributes { - | Some(attrs) => { - let attrObj = Dict.make() - attrs->Dict.toArray->Array.forEach(((k, v)) => { - attrObj->Dict.set(k, JSON.Encode.string(v)) - }) - obj->Dict.set("attributes", JSON.Encode.object(attrObj)) - } - | None => obj->Dict.set("attributes", JSON.Encode.null) - } - - // children -- array or null - switch node.children { - | Some(kids) => { - let childArr = kids->Array.map(child => JSON.Encode.object(nodeToJsonObj(child))) - obj->Dict.set("children", JSON.Encode.array(childArr)) - } - | None => obj->Dict.set("children", JSON.Encode.null) - } - - obj -} - -/// Serialise a Me AST node to a JSON string. -/// -/// Uses 2-space indentation for readability. -let toJson = (node: MeLanguage.meNode): string => { - let obj = nodeToJsonObj(node) - JSON.stringifyAny(JSON.Encode.object(obj), ~space=2)->Option.getOr("{}") -} - -// --------------------------------------------------------------------------- -// S-expression output -// --------------------------------------------------------------------------- - -/// Escape a string value for S-expression output. -/// -/// Wraps the string in double quotes and escapes internal quotes and -/// backslashes to produce valid S-expr string literals. -let escapeString = (s: string): string => { - let escaped = - s - ->String.replaceAll("\\", "\\\\") - ->String.replaceAll("\"", "\\\"") - ->String.replaceAll("\n", "\\n") - ->String.replaceAll("\r", "\\r") - ->String.replaceAll("\t", "\\t") - `"${escaped}"` -} - -/// Produce an indentation string (2 spaces per level). -let indent = (depth: int): string => { - let buf = ref("") - for _ in 0 to depth - 1 { - buf := buf.contents ++ " " - } - buf.contents -} - -/// Convert a single meNode to an S-expression string. -/// -/// The output looks like: -/// ```scheme -/// (Program -/// (Say :content "Hello!") -/// (Remember :name "x" :content "42")) -/// ``` -/// -/// Attributes are emitted as keyword-value pairs (:key "value"). -/// Children are nested sub-expressions. -let rec nodeToSexpr = (node: MeLanguage.meNode, depth: int): string => { - let pad = indent(depth) - let tag = nodeTypeToString(node.nodeType) - - // Collect attribute fragments - let attrFragments = switch node.attributes { - | Some(attrs) => - attrs - ->Dict.toArray - ->Array.map(((k, v)) => `:${k} ${escapeString(v)}`) - | None => [] - } - - // Content as attribute-style fragment - let contentFragment = switch node.content { - | Some(c) => [`:content ${escapeString(c)}`] - | None => [] - } - - let allFragments = Array.concat(attrFragments, contentFragment) - - // Children - switch node.children { - | Some(kids) if kids->Array.length > 0 => { - let fragStr = if allFragments->Array.length > 0 { - " " ++ allFragments->Array.join(" ") - } else { - "" - } - let childLines = - kids->Array.map(child => nodeToSexpr(child, depth + 1))->Array.join("\n") - `${pad}(${tag}${fragStr}\n${childLines})` - } - | _ => { - let fragStr = if allFragments->Array.length > 0 { - " " ++ allFragments->Array.join(" ") - } else { - "" - } - `${pad}(${tag}${fragStr})` - } - } -} - -/// Serialise a Me AST node to an S-expression string. -/// -/// Top-level call with depth 0. -let toSexpr = (node: MeLanguage.meNode): string => { - nodeToSexpr(node, 0) -} - -// --------------------------------------------------------------------------- -// Unified dump entry point -// --------------------------------------------------------------------------- - -/// Supported output formats for AST dumps. -type dumpFormat = - | Json - | Sexpr - -/// Parse a format string ("json" or "sexpr") into a dumpFormat. -/// -/// Returns None for unrecognised format strings. -let parseFormat = (s: string): option => { - switch s->String.toLowerCase { - | "json" => Some(Json) - | "sexpr" | "s-expr" | "sexp" => Some(Sexpr) - | _ => None - } -} - -/// Dump a Me AST node in the specified format. -/// -/// This is the main entry point called by the CLI tool (tools/ast_dump.js). -let dump = (node: MeLanguage.meNode, format: dumpFormat): string => { - switch format { - | Json => toJson(node) - | Sexpr => toSexpr(node) - } -} diff --git a/lib/ocaml/MeLanguage.affine b/lib/ocaml/MeLanguage.affine new file mode 100644 index 0000000..b70f8f7 --- /dev/null +++ b/lib/ocaml/MeLanguage.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module MeLanguage; + +// TODO: Complete semantic implementation diff --git a/lib/ocaml/MeLanguage.res b/lib/ocaml/MeLanguage.res deleted file mode 100644 index f118fe4..0000000 --- a/lib/ocaml/MeLanguage.res +++ /dev/null @@ -1,409 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 hyperpolymath -// -// Me Language - Main Entry Point -// An educational programming language for children (ages 8-12) - -/** - * Me Language Interpreter - * - * Me is a programming language designed for young learners: - * - HTML-like syntax that feels familiar - * - Visual feedback and immediate results - * - Safe sandboxed execution - * - Progressive complexity - */ - -// Me Language AST Node Types -type meNodeType = - | Program - | Say - | Remember - | Ask - | Choose - | When - | Otherwise - | Repeat - | Canvas - | Shape - | Add - | Subtract - | Stop - -// Value types in Me -type meValue = String(string) | Number(float) - -// Canvas command for drawing -type meCanvasCommand = { - shape: string, - props: Dict.t, -} - -// Me AST Node -type rec meNode = { - nodeType: meNodeType, - children: option>, - attributes: option>, - content: option, -} - -// Runtime environment for Me programs -type meEnvironment = { - mutable variables: Dict.t, - mutable output: array, - mutable canvas: array, - mutable stopped: bool, -} - -// Create a new environment -let createMeEnvironment = (): meEnvironment => { - variables: Dict.make(), - output: [], - canvas: [], - stopped: false, -} - -// Convert meValue to string -let valueToString = (value: meValue): string => { - switch value { - | String(s) => s - | Number(n) => Float.toString(n) - } -} - -// Try to parse a string as a number -let parseValue = (s: string): meValue => { - switch Float.fromString(s) { - | Some(n) => Number(n) - | None => String(s) - } -} - -// Interpolate variables in a string: "Hello {name}!" -> "Hello Alex!" -let interpolate = (text: string, env: meEnvironment): string => { - let re = %re("/\{([^}]+)\}/g") - text->String.unsafeReplaceRegExpBy0(re, (~match, ~offset as _, ~input as _) => { - // Extract variable name from {varName} - let varName = match->String.slice(~start=1, ~end=-1)->String.trim - switch env.variables->Dict.get(varName) { - | Some(value) => valueToString(value) - | None => match - } - }) -} - -// Execute a Me program node -let rec execute = (node: meNode, env: meEnvironment): unit => { - if env.stopped { - () - } else { - switch node.nodeType { - | Program => - node.children - ->Option.getOr([]) - ->Array.forEach(child => { - if !env.stopped { - execute(child, env) - } - }) - - | Say => - switch node.content { - | Some(content) => - let message = interpolate(content, env) - env.output = env.output->Array.concat([message]) - Console.log(message) - | None => () - } - - | Remember => - let name = node.attributes->Option.flatMap(attrs => attrs->Dict.get("name")) - let value = node.content - switch (name, value) { - | (Some(n), Some(v)) => env.variables->Dict.set(n, parseValue(v)) - | _ => () - } - - | Ask => - let varName = node.attributes->Option.flatMap(attrs => attrs->Dict.get("into")) - let prompt = node.content->Option.getOr("Enter a value:") - switch varName { - | Some(vn) => - Console.log("[Ask] " ++ interpolate(prompt, env)) - env.variables->Dict.set(vn, String("user-input")) - | None => () - } - - | Choose => executeChoose(node, env) - - | When | Otherwise => () - - | Repeat => - let times = node.attributes->Option.flatMap(attrs => attrs->Dict.get("times")) - switch times { - | Some(t) => - switch Int.fromString(t) { - | Some(count) => - for _ in 0 to count - 1 { - if !env.stopped { - node.children - ->Option.getOr([]) - ->Array.forEach(child => execute(child, env)) - } - } - | None => () - } - | None => () - } - - | Add => - let varName = node.attributes->Option.flatMap(attrs => attrs->Dict.get("to")) - let amount = node.content - switch (varName, amount) { - | (Some(vn), Some(amt)) => - switch (env.variables->Dict.get(vn), Float.fromString(amt)) { - | (Some(Number(current)), Some(addAmt)) => - env.variables->Dict.set(vn, Number(current +. addAmt)) - | _ => () - } - | _ => () - } - - | Subtract => - let varName = node.attributes->Option.flatMap(attrs => attrs->Dict.get("from")) - let amount = node.content - switch (varName, amount) { - | (Some(vn), Some(amt)) => - switch (env.variables->Dict.get(vn), Float.fromString(amt)) { - | (Some(Number(current)), Some(subAmt)) => - env.variables->Dict.set(vn, Number(current -. subAmt)) - | _ => () - } - | _ => () - } - - | Canvas => - let width = node.attributes->Option.flatMap(attrs => attrs->Dict.get("width"))->Option.getOr("0") - let height = node.attributes->Option.flatMap(attrs => attrs->Dict.get("height"))->Option.getOr("0") - Console.log("[Canvas] Creating " ++ width ++ "x" ++ height ++ " canvas") - node.children - ->Option.getOr([]) - ->Array.forEach(child => { - switch child.nodeType { - | Shape => - let shapeType = - child.attributes->Option.flatMap(attrs => attrs->Dict.get("type"))->Option.getOr("unknown") - let props = child.attributes->Option.getOr(Dict.make()) - env.canvas = env.canvas->Array.concat([{shape: shapeType, props: props}]) - Console.log("[Canvas] Drawing " ++ shapeType) - | _ => () - } - }) - - | Shape => () - - | Stop => env.stopped = true - } - } -} - -// Handle choose block execution -and executeChoose = (node: meNode, env: meEnvironment): unit => { - let children = node.children->Option.getOr([]) - let rec processChildren = (remaining: array): unit => { - switch remaining->Array.get(0) { - | None => () - | Some(child) => - switch child.nodeType { - | When => - let conditionMet = checkWhenCondition(child, env) - if conditionMet { - child.children - ->Option.getOr([]) - ->Array.forEach(whenChild => execute(whenChild, env)) - } else { - processChildren(remaining->Array.sliceToEnd(~start=1)) - } - | Otherwise => - child.children - ->Option.getOr([]) - ->Array.forEach(otherwiseChild => execute(otherwiseChild, env)) - | _ => processChildren(remaining->Array.sliceToEnd(~start=1)) - } - } - } - processChildren(children) -} - -// Check if a when condition is met -and checkWhenCondition = (node: meNode, env: meEnvironment): bool => { - switch node.attributes { - | None => false - | Some(attrs) => - attrs - ->Dict.toArray - ->Array.some(((key, expectedValue)) => { - if key->String.endsWith("-is") { - let varName = key->String.slice(~start=0, ~end=-3) - switch env.variables->Dict.get(varName) { - | Some(actualValue) => valueToString(actualValue) == expectedValue - | None => false - } - } else if key->String.endsWith("-is-not") { - let varName = key->String.slice(~start=0, ~end=-7) - switch env.variables->Dict.get(varName) { - | Some(actualValue) => valueToString(actualValue) != expectedValue - | None => true - } - } else { - false - } - }) - } -} - -// Demo: Run a simple Me program -let demonstrateMeLanguage = (): unit => { - Console.log("=== Me Language Demo ===\n") - Console.log("Me is a programming language for children ages 8-12.\n") - - // Example 1: Say hello - Console.log("Example 1: Hello World") - let helloProgram: meNode = { - nodeType: Program, - children: Some([{nodeType: Say, children: None, attributes: None, content: Some("Hello! I am learning to code!")}]), - attributes: None, - content: None, - } - execute(helloProgram, createMeEnvironment()) - Console.log("") - - // Example 2: Variables - Console.log("Example 2: Remembering Things") - let nameAttrs = Dict.make() - nameAttrs->Dict.set("name", "my-name") - let ageAttrs = Dict.make() - ageAttrs->Dict.set("name", "my-age") - - let variablesProgram: meNode = { - nodeType: Program, - children: Some([ - {nodeType: Remember, children: None, attributes: Some(nameAttrs), content: Some("Alex")}, - {nodeType: Remember, children: None, attributes: Some(ageAttrs), content: Some("10")}, - { - nodeType: Say, - children: None, - attributes: None, - content: Some("My name is {my-name} and I am {my-age} years old!"), - }, - ]), - attributes: None, - content: None, - } - execute(variablesProgram, createMeEnvironment()) - Console.log("") - - // Example 3: Choices - Console.log("Example 3: Making Choices") - let weatherAttrs = Dict.make() - weatherAttrs->Dict.set("name", "weather") - let sunnyAttrs = Dict.make() - sunnyAttrs->Dict.set("weather-is", "sunny") - let rainyAttrs = Dict.make() - rainyAttrs->Dict.set("weather-is", "rainy") - - let choicesProgram: meNode = { - nodeType: Program, - children: Some([ - {nodeType: Remember, children: None, attributes: Some(weatherAttrs), content: Some("sunny")}, - { - nodeType: Choose, - children: Some([ - { - nodeType: When, - children: Some([ - {nodeType: Say, children: None, attributes: None, content: Some("Let's go outside!")}, - ]), - attributes: Some(sunnyAttrs), - content: None, - }, - { - nodeType: When, - children: Some([ - {nodeType: Say, children: None, attributes: None, content: Some("Let's read a book!")}, - ]), - attributes: Some(rainyAttrs), - content: None, - }, - { - nodeType: Otherwise, - children: Some([ - {nodeType: Say, children: None, attributes: None, content: Some("What's the weather like?")}, - ]), - attributes: None, - content: None, - }, - ]), - attributes: None, - content: None, - }, - ]), - attributes: None, - content: None, - } - execute(choicesProgram, createMeEnvironment()) - Console.log("") - - // Example 4: Repeating - Console.log("Example 4: Repeating Things") - let repeatAttrs = Dict.make() - repeatAttrs->Dict.set("times", "3") - - let repeatProgram: meNode = { - nodeType: Program, - children: Some([ - { - nodeType: Repeat, - children: Some([{nodeType: Say, children: None, attributes: None, content: Some("Hip hip hooray!")}]), - attributes: Some(repeatAttrs), - content: None, - }, - ]), - attributes: None, - content: None, - } - execute(repeatProgram, createMeEnvironment()) - Console.log("") - - // Example 5: Counting - Console.log("Example 5: Counting") - let scoreAttrs = Dict.make() - scoreAttrs->Dict.set("name", "score") - let addAttrs = Dict.make() - addAttrs->Dict.set("to", "score") - let countRepeatAttrs = Dict.make() - countRepeatAttrs->Dict.set("times", "3") - - let countingProgram: meNode = { - nodeType: Program, - children: Some([ - {nodeType: Remember, children: None, attributes: Some(scoreAttrs), content: Some("0")}, - { - nodeType: Repeat, - children: Some([ - {nodeType: Add, children: None, attributes: Some(addAttrs), content: Some("10")}, - {nodeType: Say, children: None, attributes: None, content: Some("Score is now: {score}")}, - ]), - attributes: Some(countRepeatAttrs), - content: None, - }, - ]), - attributes: None, - content: None, - } - execute(countingProgram, createMeEnvironment()) - Console.log("") - - Console.log("=== Demo Complete ===") - Console.log("\nMe makes programming fun and approachable for kids!") -} diff --git a/lib/ocaml/MeParser.affine b/lib/ocaml/MeParser.affine new file mode 100644 index 0000000..3d8742f --- /dev/null +++ b/lib/ocaml/MeParser.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module MeParser; + +// TODO: Complete semantic implementation diff --git a/lib/ocaml/MeParser.res b/lib/ocaml/MeParser.res deleted file mode 100644 index 621e5da..0000000 --- a/lib/ocaml/MeParser.res +++ /dev/null @@ -1,611 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell -// -// Me Language - Parser Module -// -// Parses Me-Dialect source code (XML/HTML-like tags) into the meNode AST. -// Designed for children ages 8-12, so error messages are friendly and -// the parser is intentionally lenient where possible. -// -// ## Supported tags -// -// - text -- Print text (with {var} interpolation) -// - val -- Store a value -// - prompt -- Ask the user for input -// - ... -- Conditional branching -// - ... -- Branch condition -// - ... -- Default branch -// - ... -- Loop n times -// - ... -- Drawing surface -// - -- Draw a shape (self-closing) -// - amount -- Add to a variable -// - amount -- Subtract from a variable -// - -- Stop execution -// -// ## Error handling -// -// Returns Result with friendly error messages that tell -// the child what went wrong and how to fix it. - -// --------------------------------------------------------------------------- -// Parser position tracking -// --------------------------------------------------------------------------- - -/// Tracks where we are in the source string during parsing. -type parserState = { - source: string, - mutable pos: int, -} - -/// Create a new parser state from source code. -let makeState = (source: string): parserState => { - source, - pos: 0, -} - -/// Check whether we have reached the end of the source. -let isAtEnd = (state: parserState): bool => { - state.pos >= state.source->String.length -} - -/// Peek at the current character without advancing. -let peek = (state: parserState): option => { - if isAtEnd(state) { - None - } else { - Some(state.source->String.charAt(state.pos)) - } -} - -/// Advance the position by n characters. -let advance = (state: parserState, n: int): unit => { - state.pos = state.pos + n -} - -/// Check whether the source at the current position starts with a prefix. -let startsWith = (state: parserState, prefix: string): bool => { - let remaining = state.source->String.sliceToEnd(~start=state.pos) - remaining->String.startsWith(prefix) -} - -/// Skip whitespace characters (spaces, tabs, newlines, carriage returns). -let skipWhitespace = (state: parserState): unit => { - let len = state.source->String.length - let continue = ref(true) - while continue.contents && state.pos < len { - let ch = state.source->String.charAt(state.pos) - if ch == " " || ch == "\t" || ch == "\n" || ch == "\r" { - state.pos = state.pos + 1 - } else { - continue := false - } - } -} - -/// Skip an HTML/XML comment: -let skipComment = (state: parserState): unit => { - if startsWith(state, ", just skip to end - if !found.contents { - state.pos = len - } - } -} - -/// Skip all whitespace and comments. -let skipWhitespaceAndComments = (state: parserState): unit => { - let changed = ref(true) - while changed.contents { - let before = state.pos - skipWhitespace(state) - if startsWith(state, " - Hello! - - ` - switch parseOk(source) { - | Some(prog) => - let kids = prog.children->Option.getOr([]) - assertEqual(kids->Array.length, 1, "comments: only 1 real node") && { - let child = kids->Array.getUnsafe(0) - assertEqual(child.content, Some("Hello!"), "comments: correct content") - } - | None => false - } -} - -// --------------------------------------------------------------------------- -// Single-quoted attributes -// --------------------------------------------------------------------------- - -let testParseSingleQuotedAttr = () => { - switch parseOk("blue") { - | Some(prog) => - let kids = prog.children->Option.getOr([]) - let child = kids->Array.getUnsafe(0) - let nameAttr = switch child.attributes { - | Some(attrs) => attrs->Dict.get("name") - | None => None - } - assertEqual(nameAttr, Some("color"), "single-quoted attribute") - | None => false - } -} - -// --------------------------------------------------------------------------- -// Error cases -// --------------------------------------------------------------------------- - -let testErrorUnknownTag = () => { - parseErr("Hello!") -} - -let testErrorUnclosedTag = () => { - parseErr("Hello!") -} - -let testErrorMismatchedClose = () => { - parseErr("Hello!") -} - -let testErrorTextOutsideTag = () => { - parseErr("Hello world") -} - -let testErrorUnclosedAttribute = () => { - parseErr(`Round trip works!") { - | Ok(prog) => - let env = runProgram(prog) - assertEqual(env.output, ["Round trip works!"], "parse+execute: say output") - | Error(_) => false - } -} - -let testParseAndExecuteRememberSay = () => { - let source = ` - cat - I have a {pet}! - ` - switch MeParser.parse(source) { - | Ok(prog) => - let env = runProgram(prog) - assertEqual(env.output, ["I have a cat!"], "parse+execute: interpolation") - | Error(_) => false - } -} - -let testParseAndExecuteRepeat = () => { - let source = ` - - Go! - - ` - switch MeParser.parse(source) { - | Ok(prog) => - let env = runProgram(prog) - assertEqual(env.output, ["Go!", "Go!", "Go!"], "parse+execute: repeat 3x") - | Error(_) => false - } -} - -// --------------------------------------------------------------------------- -// Run all tests -// --------------------------------------------------------------------------- - -let run = (): array => { - [ - runTest("Parser: empty string", testEmptyString), - runTest("Parser: whitespace only", testWhitespaceOnly), - runTest("Parser: basic", testParseSay), - runTest("Parser: with interpolation", testParseSayInterpolation), - runTest("Parser: empty content", testParseSayEmpty), - runTest("Parser: ", testParseRemember), - runTest("Parser: ", testParseAsk), - runTest("Parser: ", testParseAdd), - runTest("Parser: ", testParseSubtract), - runTest("Parser: ", testParseStop), - runTest("Parser: with space", testParseStopWithSpaces), - runTest("Parser: ", testParseRepeat), - runTest("Parser: //", testParseChoose), - runTest("Parser: /", testParseCanvas), - runTest("Parser: multiple top-level nodes", testParseMultiNode), - runTest("Parser: full counting program", testParseCountingProgram), - runTest("Parser: comments are skipped", testParseComments), - runTest("Parser: single-quoted attributes", testParseSingleQuotedAttr), - runTest("Parser: error on unknown tag", testErrorUnknownTag), - runTest("Parser: error on unclosed tag", testErrorUnclosedTag), - runTest("Parser: error on mismatched close", testErrorMismatchedClose), - runTest("Parser: error on text outside tag", testErrorTextOutsideTag), - runTest("Parser: error on unclosed attribute", testErrorUnclosedAttribute), - runTest("Parser: parse+execute ", testParseAndExecuteSay), - runTest("Parser: parse+execute remember+say", testParseAndExecuteRememberSay), - runTest("Parser: parse+execute ", testParseAndExecuteRepeat), - ] -} diff --git a/lib/ocaml/MeTest_Repetition.affine b/lib/ocaml/MeTest_Repetition.affine new file mode 100644 index 0000000..e44a02f --- /dev/null +++ b/lib/ocaml/MeTest_Repetition.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module MeTest_Repetition; + +// TODO: Complete semantic implementation diff --git a/lib/ocaml/MeTest_Repetition.res b/lib/ocaml/MeTest_Repetition.res deleted file mode 100644 index f3847c5..0000000 --- a/lib/ocaml/MeTest_Repetition.res +++ /dev/null @@ -1,214 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2026 hyperpolymath -// -// Me Language Tests: Repetition (repeat with times) -// Covers repeat loops, counting within loops, nested repeats, -// and edge cases like zero iterations. - -open MeTestUtils - -// -- Basic repeat ---------------------------------------------------- - -let testRepeatThreeTimes = () => { - let env = runProgram(program([repeat(3, [say("Go!")])])) - assertEqual(env.output, ["Go!", "Go!", "Go!"], "repeat 3 times") -} - -let testRepeatOnce = () => { - let env = runProgram(program([repeat(1, [say("Once")])])) - assertEqual(env.output, ["Once"], "repeat 1 time") -} - -let testRepeatZeroTimes = () => { - let env = runProgram(program([repeat(0, [say("Never")])])) - assertEqual(env.output, [], "repeat 0 times produces nothing") -} - -// -- Counting inside repeat ------------------------------------------ - -let testRepeatWithCounter = () => { - let env = runProgram( - program([ - remember("count", "0"), - repeat(5, [add("count", "1")]), - say("Count: {count}"), - ]), - ) - assertEqual(env.output, ["Count: 5"], "repeat with counter") -} - -let testRepeatWithAdd = () => { - let env = runProgram( - program([ - remember("score", "0"), - repeat(3, [add("score", "10")]), - ]), - ) - assertEqual( - env.variables->Dict.get("score"), - Some(MeLanguage.Number(30.0)), - "repeat adds 10 three times = 30", - ) -} - -let testRepeatWithSubtract = () => { - let env = runProgram( - program([ - remember("hp", "100"), - repeat(4, [subtract("hp", "15")]), - ]), - ) - assertEqual( - env.variables->Dict.get("hp"), - Some(MeLanguage.Number(40.0)), - "repeat subtracts 15 four times = 40", - ) -} - -// -- Multiple children in repeat ------------------------------------- - -let testRepeatMultipleChildren = () => { - let env = runProgram( - program([ - remember("n", "0"), - repeat(2, [ - add("n", "1"), - say("Step {n}"), - ]), - ]), - ) - assertEqual(env.output, ["Step 1", "Step 2"], "repeat with add and say") -} - -// -- Nested repeat --------------------------------------------------- - -let testNestedRepeat = () => { - let env = runProgram( - program([ - remember("total", "0"), - repeat(3, [ - repeat(2, [add("total", "1")]), - ]), - ]), - ) - assertEqual( - env.variables->Dict.get("total"), - Some(MeLanguage.Number(6.0)), - "nested repeat 3x2 = 6", - ) -} - -// -- Repeat with stop ------------------------------------------------ - -let testRepeatStopsEarly = () => { - let env = runProgram( - program([ - remember("i", "0"), - repeat(10, [ - add("i", "1"), - say("Iteration {i}"), - // Stop after first iteration via a conditional - choose([when_("i", "2", [stop()])]), - ]), - ]), - ) - // Should run iterations 1 and 2, then stop - assertEqual( - env.output, - ["Iteration 1", "Iteration 2"], - "repeat stops early with stop", - ) -} - -// -- Repeat with invalid times --------------------------------------- - -let testRepeatNonNumericTimes = () => { - // Non-numeric times attribute should be ignored - let attrs = Dict.make() - attrs->Dict.set("times", "abc") - let node = makeNode( - ~nodeType=MeLanguage.Repeat, - ~attributes=Some(attrs), - ~children=Some([say("never")]), - (), - ) - let env = runProgram(program([node])) - assertEqual(env.output, [], "repeat with non-numeric times does nothing") -} - -let testRepeatNoTimesAttribute = () => { - // Repeat with no times attribute should do nothing - let node = makeNode( - ~nodeType=MeLanguage.Repeat, - ~children=Some([say("never")]), - (), - ) - let env = runProgram(program([node])) - assertEqual(env.output, [], "repeat without times attribute does nothing") -} - -let testRepeatNoChildren = () => { - // Repeat with times but no children should do nothing - let env = runProgram(program([repeat(5, [])])) - assertEqual(env.output, [], "repeat with no children does nothing") -} - -// -- Large repeat ---------------------------------------------------- - -let testRepeatLarge = () => { - let env = runProgram( - program([ - remember("sum", "0"), - repeat(100, [add("sum", "1")]), - ]), - ) - assertEqual( - env.variables->Dict.get("sum"), - Some(MeLanguage.Number(100.0)), - "repeat 100 times counting", - ) -} - -// -- Repeat with choose inside --------------------------------------- - -let testRepeatWithChoose = () => { - let env = runProgram( - program([ - remember("n", "0"), - repeat(4, [ - add("n", "1"), - choose([ - when_("n", "1", [say("one")]), - when_("n", "2", [say("two")]), - otherwise([say("more")]), - ]), - ]), - ]), - ) - assertEqual( - env.output, - ["one", "two", "more", "more"], - "repeat with choose inside", - ) -} - -// -- Run all tests --------------------------------------------------- - -let run = (): array => { - [ - runTest("Repeat: three times", testRepeatThreeTimes), - runTest("Repeat: once", testRepeatOnce), - runTest("Repeat: zero times", testRepeatZeroTimes), - runTest("Repeat: with counter", testRepeatWithCounter), - runTest("Repeat: with add", testRepeatWithAdd), - runTest("Repeat: with subtract", testRepeatWithSubtract), - runTest("Repeat: multiple children", testRepeatMultipleChildren), - runTest("Repeat: nested", testNestedRepeat), - runTest("Repeat: stops early", testRepeatStopsEarly), - runTest("Repeat: non-numeric times", testRepeatNonNumericTimes), - runTest("Repeat: no times attribute", testRepeatNoTimesAttribute), - runTest("Repeat: no children", testRepeatNoChildren), - runTest("Repeat: large (100)", testRepeatLarge), - runTest("Repeat: with choose inside", testRepeatWithChoose), - ] -} diff --git a/lib/ocaml/MeTest_ValueAndInterpolation.affine b/lib/ocaml/MeTest_ValueAndInterpolation.affine new file mode 100644 index 0000000..500df7f --- /dev/null +++ b/lib/ocaml/MeTest_ValueAndInterpolation.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module MeTest_ValueAndInterpolation; + +// TODO: Complete semantic implementation diff --git a/lib/ocaml/MeTest_ValueAndInterpolation.res b/lib/ocaml/MeTest_ValueAndInterpolation.res deleted file mode 100644 index dc5a572..0000000 --- a/lib/ocaml/MeTest_ValueAndInterpolation.res +++ /dev/null @@ -1,129 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2026 hyperpolymath -// -// Me Language Tests: Value Parsing and String Interpolation -// Covers parseValue, valueToString, and interpolate functions. - -open MeTestUtils - -// -- parseValue ------------------------------------------------------ - -let testParseValueInteger = () => { - let v = MeLanguage.parseValue("42") - assertEqual(v, MeLanguage.Number(42.0), "parseValue integer") -} - -let testParseValueFloat = () => { - let v = MeLanguage.parseValue("3.14") - assertEqual(v, MeLanguage.Number(3.14), "parseValue float") -} - -let testParseValueNegative = () => { - let v = MeLanguage.parseValue("-7") - assertEqual(v, MeLanguage.Number(-7.0), "parseValue negative") -} - -let testParseValueZero = () => { - let v = MeLanguage.parseValue("0") - assertEqual(v, MeLanguage.Number(0.0), "parseValue zero") -} - -let testParseValueString = () => { - let v = MeLanguage.parseValue("hello") - assertEqual(v, MeLanguage.String("hello"), "parseValue non-numeric string") -} - -let testParseValueEmptyString = () => { - let v = MeLanguage.parseValue("") - assertEqual(v, MeLanguage.String(""), "parseValue empty string") -} - -let testParseValueMixedText = () => { - let v = MeLanguage.parseValue("abc123") - assertEqual(v, MeLanguage.String("abc123"), "parseValue mixed text") -} - -// -- valueToString --------------------------------------------------- - -let testValueToStringStr = () => { - let s = MeLanguage.valueToString(MeLanguage.String("hi")) - assertEqual(s, "hi", "valueToString String") -} - -let testValueToStringNum = () => { - let s = MeLanguage.valueToString(MeLanguage.Number(5.0)) - assertEqual(s, "5", "valueToString Number(5)") -} - -// -- interpolate ----------------------------------------------------- - -let testInterpolateSingleVar = () => { - let env = MeLanguage.createMeEnvironment() - env.variables->Dict.set("name", MeLanguage.String("Alex")) - let result = MeLanguage.interpolate("Hello {name}!", env) - assertEqual(result, "Hello Alex!", "interpolate single variable") -} - -let testInterpolateMultipleVars = () => { - let env = MeLanguage.createMeEnvironment() - env.variables->Dict.set("name", MeLanguage.String("Alex")) - env.variables->Dict.set("age", MeLanguage.Number(10.0)) - let result = MeLanguage.interpolate("I am {name}, age {age}.", env) - assertEqual(result, "I am Alex, age 10.", "interpolate multiple variables") -} - -let testInterpolateNoVars = () => { - let env = MeLanguage.createMeEnvironment() - let result = MeLanguage.interpolate("No variables here.", env) - assertEqual(result, "No variables here.", "interpolate no variables") -} - -let testInterpolateUnknownVar = () => { - let env = MeLanguage.createMeEnvironment() - let result = MeLanguage.interpolate("Hello {unknown}!", env) - assertEqual(result, "Hello {unknown}!", "interpolate unknown variable kept") -} - -let testInterpolateAdjacentVars = () => { - let env = MeLanguage.createMeEnvironment() - env.variables->Dict.set("a", MeLanguage.String("X")) - env.variables->Dict.set("b", MeLanguage.String("Y")) - let result = MeLanguage.interpolate("{a}{b}", env) - assertEqual(result, "XY", "interpolate adjacent variables") -} - -let testInterpolateEmptyString = () => { - let env = MeLanguage.createMeEnvironment() - let result = MeLanguage.interpolate("", env) - assertEqual(result, "", "interpolate empty string") -} - -let testInterpolateBracesNoVar = () => { - let env = MeLanguage.createMeEnvironment() - let result = MeLanguage.interpolate("Use {} for fun", env) - // {} contains empty name which won't match any variable - assertEqual(result == "Use {} for fun" || result == "Use for fun", true, "interpolate empty braces") -} - -// -- Run all tests --------------------------------------------------- - -let run = (): array => { - [ - runTest("parseValue: integer", testParseValueInteger), - runTest("parseValue: float", testParseValueFloat), - runTest("parseValue: negative", testParseValueNegative), - runTest("parseValue: zero", testParseValueZero), - runTest("parseValue: string", testParseValueString), - runTest("parseValue: empty string", testParseValueEmptyString), - runTest("parseValue: mixed text", testParseValueMixedText), - runTest("valueToString: String", testValueToStringStr), - runTest("valueToString: Number", testValueToStringNum), - runTest("interpolate: single var", testInterpolateSingleVar), - runTest("interpolate: multiple vars", testInterpolateMultipleVars), - runTest("interpolate: no vars", testInterpolateNoVars), - runTest("interpolate: unknown var", testInterpolateUnknownVar), - runTest("interpolate: adjacent vars", testInterpolateAdjacentVars), - runTest("interpolate: empty string", testInterpolateEmptyString), - runTest("interpolate: empty braces", testInterpolateBracesNoVar), - ] -} diff --git a/rescript.json b/rescript.json deleted file mode 100644 index 0d63605..0000000 --- a/rescript.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "me-dialect-playground", - "version": "0.1.0", - "sources": [ - { - "dir": "src", - "subdirs": true - }, - { - "dir": "test", - "subdirs": true, - "type": "dev" - } - ], - "package-specs": [ - { - "module": "es6", - "in-source": true - } - ], - "suffix": ".res.js", - "bs-dependencies": ["@rescript/core"], - "bsc-flags": ["-open RescriptCore"] -} diff --git a/src/Main.affine b/src/Main.affine new file mode 100644 index 0000000..d410d4c --- /dev/null +++ b/src/Main.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Main; + +// TODO: Complete semantic implementation diff --git a/src/Main.res b/src/Main.res deleted file mode 100644 index 391caec..0000000 --- a/src/Main.res +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 hyperpolymath -// -// Me Language - Main Entry Point - -// Re-export all types and functions from MeLanguage -include MeLanguage - -// Run demo when executed directly -let main = () => { - demonstrateMeLanguage() -} - -// Check if running as main module -// Note: This is handled by the Deno wrapper diff --git a/src/MeAstDump.affine b/src/MeAstDump.affine new file mode 100644 index 0000000..7f2c9ef --- /dev/null +++ b/src/MeAstDump.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module MeAstDump; + +// TODO: Complete semantic implementation diff --git a/src/MeAstDump.res b/src/MeAstDump.res deleted file mode 100644 index 8fa8efb..0000000 --- a/src/MeAstDump.res +++ /dev/null @@ -1,208 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell -// -// Me Language - AST Dump Module -// -// Serialises a Me-Dialect AST to JSON or S-expression format. -// Used for debugging, tooling integration, and compiler pipeline inspection. -// -// ## Supported output formats -// -// - **JSON**: Standard JSON representation using Deno's built-in JSON.stringify, -// suitable for machine consumption and piping to jq. -// - **S-expr**: Lisp-style S-expressions for human-readable inspection and -// integration with Scheme/Guile tooling (e.g. STATE.scm pipelines). - -/// Convert a meNodeType variant to its canonical string tag. -/// -/// These tags match the HTML-like syntax used in .me.js example files -/// (e.g. "Program", "Say", "Remember"). -let nodeTypeToString = (nt: MeLanguage.meNodeType): string => { - switch nt { - | Program => "Program" - | Say => "Say" - | Remember => "Remember" - | Ask => "Ask" - | Choose => "Choose" - | When => "When" - | Otherwise => "Otherwise" - | Repeat => "Repeat" - | Canvas => "Canvas" - | Shape => "Shape" - | Add => "Add" - | Subtract => "Subtract" - | Stop => "Stop" - } -} - -// --------------------------------------------------------------------------- -// JSON output -// --------------------------------------------------------------------------- - -/// Convert a single meNode to a plain JS object suitable for JSON.stringify. -/// -/// Recursively converts children. Attributes are emitted as a plain object -/// (or null). Content and children use null when absent. -let rec nodeToJsonObj = (node: MeLanguage.meNode): Dict.t => { - let obj = Dict.make() - - // nodeType -- always present - obj->Dict.set("nodeType", JSON.Encode.string(nodeTypeToString(node.nodeType))) - - // content -- string or null - switch node.content { - | Some(c) => obj->Dict.set("content", JSON.Encode.string(c)) - | None => obj->Dict.set("content", JSON.Encode.null) - } - - // attributes -- object or null - switch node.attributes { - | Some(attrs) => { - let attrObj = Dict.make() - attrs->Dict.toArray->Array.forEach(((k, v)) => { - attrObj->Dict.set(k, JSON.Encode.string(v)) - }) - obj->Dict.set("attributes", JSON.Encode.object(attrObj)) - } - | None => obj->Dict.set("attributes", JSON.Encode.null) - } - - // children -- array or null - switch node.children { - | Some(kids) => { - let childArr = kids->Array.map(child => JSON.Encode.object(nodeToJsonObj(child))) - obj->Dict.set("children", JSON.Encode.array(childArr)) - } - | None => obj->Dict.set("children", JSON.Encode.null) - } - - obj -} - -/// Serialise a Me AST node to a JSON string. -/// -/// Uses 2-space indentation for readability. -let toJson = (node: MeLanguage.meNode): string => { - let obj = nodeToJsonObj(node) - JSON.stringifyAny(JSON.Encode.object(obj), ~space=2)->Option.getOr("{}") -} - -// --------------------------------------------------------------------------- -// S-expression output -// --------------------------------------------------------------------------- - -/// Escape a string value for S-expression output. -/// -/// Wraps the string in double quotes and escapes internal quotes and -/// backslashes to produce valid S-expr string literals. -let escapeString = (s: string): string => { - let escaped = - s - ->String.replaceAll("\\", "\\\\") - ->String.replaceAll("\"", "\\\"") - ->String.replaceAll("\n", "\\n") - ->String.replaceAll("\r", "\\r") - ->String.replaceAll("\t", "\\t") - `"${escaped}"` -} - -/// Produce an indentation string (2 spaces per level). -let indent = (depth: int): string => { - let buf = ref("") - for _ in 0 to depth - 1 { - buf := buf.contents ++ " " - } - buf.contents -} - -/// Convert a single meNode to an S-expression string. -/// -/// The output looks like: -/// ```scheme -/// (Program -/// (Say :content "Hello!") -/// (Remember :name "x" :content "42")) -/// ``` -/// -/// Attributes are emitted as keyword-value pairs (:key "value"). -/// Children are nested sub-expressions. -let rec nodeToSexpr = (node: MeLanguage.meNode, depth: int): string => { - let pad = indent(depth) - let tag = nodeTypeToString(node.nodeType) - - // Collect attribute fragments - let attrFragments = switch node.attributes { - | Some(attrs) => - attrs - ->Dict.toArray - ->Array.map(((k, v)) => `:${k} ${escapeString(v)}`) - | None => [] - } - - // Content as attribute-style fragment - let contentFragment = switch node.content { - | Some(c) => [`:content ${escapeString(c)}`] - | None => [] - } - - let allFragments = Array.concat(attrFragments, contentFragment) - - // Children - switch node.children { - | Some(kids) if kids->Array.length > 0 => { - let fragStr = if allFragments->Array.length > 0 { - " " ++ allFragments->Array.join(" ") - } else { - "" - } - let childLines = - kids->Array.map(child => nodeToSexpr(child, depth + 1))->Array.join("\n") - `${pad}(${tag}${fragStr}\n${childLines})` - } - | _ => { - let fragStr = if allFragments->Array.length > 0 { - " " ++ allFragments->Array.join(" ") - } else { - "" - } - `${pad}(${tag}${fragStr})` - } - } -} - -/// Serialise a Me AST node to an S-expression string. -/// -/// Top-level call with depth 0. -let toSexpr = (node: MeLanguage.meNode): string => { - nodeToSexpr(node, 0) -} - -// --------------------------------------------------------------------------- -// Unified dump entry point -// --------------------------------------------------------------------------- - -/// Supported output formats for AST dumps. -type dumpFormat = - | Json - | Sexpr - -/// Parse a format string ("json" or "sexpr") into a dumpFormat. -/// -/// Returns None for unrecognised format strings. -let parseFormat = (s: string): option => { - switch s->String.toLowerCase { - | "json" => Some(Json) - | "sexpr" | "s-expr" | "sexp" => Some(Sexpr) - | _ => None - } -} - -/// Dump a Me AST node in the specified format. -/// -/// This is the main entry point called by the CLI tool (tools/ast_dump.js). -let dump = (node: MeLanguage.meNode, format: dumpFormat): string => { - switch format { - | Json => toJson(node) - | Sexpr => toSexpr(node) - } -} diff --git a/src/MeLanguage.affine b/src/MeLanguage.affine new file mode 100644 index 0000000..b70f8f7 --- /dev/null +++ b/src/MeLanguage.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module MeLanguage; + +// TODO: Complete semantic implementation diff --git a/src/MeLanguage.res b/src/MeLanguage.res deleted file mode 100644 index f118fe4..0000000 --- a/src/MeLanguage.res +++ /dev/null @@ -1,409 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 hyperpolymath -// -// Me Language - Main Entry Point -// An educational programming language for children (ages 8-12) - -/** - * Me Language Interpreter - * - * Me is a programming language designed for young learners: - * - HTML-like syntax that feels familiar - * - Visual feedback and immediate results - * - Safe sandboxed execution - * - Progressive complexity - */ - -// Me Language AST Node Types -type meNodeType = - | Program - | Say - | Remember - | Ask - | Choose - | When - | Otherwise - | Repeat - | Canvas - | Shape - | Add - | Subtract - | Stop - -// Value types in Me -type meValue = String(string) | Number(float) - -// Canvas command for drawing -type meCanvasCommand = { - shape: string, - props: Dict.t, -} - -// Me AST Node -type rec meNode = { - nodeType: meNodeType, - children: option>, - attributes: option>, - content: option, -} - -// Runtime environment for Me programs -type meEnvironment = { - mutable variables: Dict.t, - mutable output: array, - mutable canvas: array, - mutable stopped: bool, -} - -// Create a new environment -let createMeEnvironment = (): meEnvironment => { - variables: Dict.make(), - output: [], - canvas: [], - stopped: false, -} - -// Convert meValue to string -let valueToString = (value: meValue): string => { - switch value { - | String(s) => s - | Number(n) => Float.toString(n) - } -} - -// Try to parse a string as a number -let parseValue = (s: string): meValue => { - switch Float.fromString(s) { - | Some(n) => Number(n) - | None => String(s) - } -} - -// Interpolate variables in a string: "Hello {name}!" -> "Hello Alex!" -let interpolate = (text: string, env: meEnvironment): string => { - let re = %re("/\{([^}]+)\}/g") - text->String.unsafeReplaceRegExpBy0(re, (~match, ~offset as _, ~input as _) => { - // Extract variable name from {varName} - let varName = match->String.slice(~start=1, ~end=-1)->String.trim - switch env.variables->Dict.get(varName) { - | Some(value) => valueToString(value) - | None => match - } - }) -} - -// Execute a Me program node -let rec execute = (node: meNode, env: meEnvironment): unit => { - if env.stopped { - () - } else { - switch node.nodeType { - | Program => - node.children - ->Option.getOr([]) - ->Array.forEach(child => { - if !env.stopped { - execute(child, env) - } - }) - - | Say => - switch node.content { - | Some(content) => - let message = interpolate(content, env) - env.output = env.output->Array.concat([message]) - Console.log(message) - | None => () - } - - | Remember => - let name = node.attributes->Option.flatMap(attrs => attrs->Dict.get("name")) - let value = node.content - switch (name, value) { - | (Some(n), Some(v)) => env.variables->Dict.set(n, parseValue(v)) - | _ => () - } - - | Ask => - let varName = node.attributes->Option.flatMap(attrs => attrs->Dict.get("into")) - let prompt = node.content->Option.getOr("Enter a value:") - switch varName { - | Some(vn) => - Console.log("[Ask] " ++ interpolate(prompt, env)) - env.variables->Dict.set(vn, String("user-input")) - | None => () - } - - | Choose => executeChoose(node, env) - - | When | Otherwise => () - - | Repeat => - let times = node.attributes->Option.flatMap(attrs => attrs->Dict.get("times")) - switch times { - | Some(t) => - switch Int.fromString(t) { - | Some(count) => - for _ in 0 to count - 1 { - if !env.stopped { - node.children - ->Option.getOr([]) - ->Array.forEach(child => execute(child, env)) - } - } - | None => () - } - | None => () - } - - | Add => - let varName = node.attributes->Option.flatMap(attrs => attrs->Dict.get("to")) - let amount = node.content - switch (varName, amount) { - | (Some(vn), Some(amt)) => - switch (env.variables->Dict.get(vn), Float.fromString(amt)) { - | (Some(Number(current)), Some(addAmt)) => - env.variables->Dict.set(vn, Number(current +. addAmt)) - | _ => () - } - | _ => () - } - - | Subtract => - let varName = node.attributes->Option.flatMap(attrs => attrs->Dict.get("from")) - let amount = node.content - switch (varName, amount) { - | (Some(vn), Some(amt)) => - switch (env.variables->Dict.get(vn), Float.fromString(amt)) { - | (Some(Number(current)), Some(subAmt)) => - env.variables->Dict.set(vn, Number(current -. subAmt)) - | _ => () - } - | _ => () - } - - | Canvas => - let width = node.attributes->Option.flatMap(attrs => attrs->Dict.get("width"))->Option.getOr("0") - let height = node.attributes->Option.flatMap(attrs => attrs->Dict.get("height"))->Option.getOr("0") - Console.log("[Canvas] Creating " ++ width ++ "x" ++ height ++ " canvas") - node.children - ->Option.getOr([]) - ->Array.forEach(child => { - switch child.nodeType { - | Shape => - let shapeType = - child.attributes->Option.flatMap(attrs => attrs->Dict.get("type"))->Option.getOr("unknown") - let props = child.attributes->Option.getOr(Dict.make()) - env.canvas = env.canvas->Array.concat([{shape: shapeType, props: props}]) - Console.log("[Canvas] Drawing " ++ shapeType) - | _ => () - } - }) - - | Shape => () - - | Stop => env.stopped = true - } - } -} - -// Handle choose block execution -and executeChoose = (node: meNode, env: meEnvironment): unit => { - let children = node.children->Option.getOr([]) - let rec processChildren = (remaining: array): unit => { - switch remaining->Array.get(0) { - | None => () - | Some(child) => - switch child.nodeType { - | When => - let conditionMet = checkWhenCondition(child, env) - if conditionMet { - child.children - ->Option.getOr([]) - ->Array.forEach(whenChild => execute(whenChild, env)) - } else { - processChildren(remaining->Array.sliceToEnd(~start=1)) - } - | Otherwise => - child.children - ->Option.getOr([]) - ->Array.forEach(otherwiseChild => execute(otherwiseChild, env)) - | _ => processChildren(remaining->Array.sliceToEnd(~start=1)) - } - } - } - processChildren(children) -} - -// Check if a when condition is met -and checkWhenCondition = (node: meNode, env: meEnvironment): bool => { - switch node.attributes { - | None => false - | Some(attrs) => - attrs - ->Dict.toArray - ->Array.some(((key, expectedValue)) => { - if key->String.endsWith("-is") { - let varName = key->String.slice(~start=0, ~end=-3) - switch env.variables->Dict.get(varName) { - | Some(actualValue) => valueToString(actualValue) == expectedValue - | None => false - } - } else if key->String.endsWith("-is-not") { - let varName = key->String.slice(~start=0, ~end=-7) - switch env.variables->Dict.get(varName) { - | Some(actualValue) => valueToString(actualValue) != expectedValue - | None => true - } - } else { - false - } - }) - } -} - -// Demo: Run a simple Me program -let demonstrateMeLanguage = (): unit => { - Console.log("=== Me Language Demo ===\n") - Console.log("Me is a programming language for children ages 8-12.\n") - - // Example 1: Say hello - Console.log("Example 1: Hello World") - let helloProgram: meNode = { - nodeType: Program, - children: Some([{nodeType: Say, children: None, attributes: None, content: Some("Hello! I am learning to code!")}]), - attributes: None, - content: None, - } - execute(helloProgram, createMeEnvironment()) - Console.log("") - - // Example 2: Variables - Console.log("Example 2: Remembering Things") - let nameAttrs = Dict.make() - nameAttrs->Dict.set("name", "my-name") - let ageAttrs = Dict.make() - ageAttrs->Dict.set("name", "my-age") - - let variablesProgram: meNode = { - nodeType: Program, - children: Some([ - {nodeType: Remember, children: None, attributes: Some(nameAttrs), content: Some("Alex")}, - {nodeType: Remember, children: None, attributes: Some(ageAttrs), content: Some("10")}, - { - nodeType: Say, - children: None, - attributes: None, - content: Some("My name is {my-name} and I am {my-age} years old!"), - }, - ]), - attributes: None, - content: None, - } - execute(variablesProgram, createMeEnvironment()) - Console.log("") - - // Example 3: Choices - Console.log("Example 3: Making Choices") - let weatherAttrs = Dict.make() - weatherAttrs->Dict.set("name", "weather") - let sunnyAttrs = Dict.make() - sunnyAttrs->Dict.set("weather-is", "sunny") - let rainyAttrs = Dict.make() - rainyAttrs->Dict.set("weather-is", "rainy") - - let choicesProgram: meNode = { - nodeType: Program, - children: Some([ - {nodeType: Remember, children: None, attributes: Some(weatherAttrs), content: Some("sunny")}, - { - nodeType: Choose, - children: Some([ - { - nodeType: When, - children: Some([ - {nodeType: Say, children: None, attributes: None, content: Some("Let's go outside!")}, - ]), - attributes: Some(sunnyAttrs), - content: None, - }, - { - nodeType: When, - children: Some([ - {nodeType: Say, children: None, attributes: None, content: Some("Let's read a book!")}, - ]), - attributes: Some(rainyAttrs), - content: None, - }, - { - nodeType: Otherwise, - children: Some([ - {nodeType: Say, children: None, attributes: None, content: Some("What's the weather like?")}, - ]), - attributes: None, - content: None, - }, - ]), - attributes: None, - content: None, - }, - ]), - attributes: None, - content: None, - } - execute(choicesProgram, createMeEnvironment()) - Console.log("") - - // Example 4: Repeating - Console.log("Example 4: Repeating Things") - let repeatAttrs = Dict.make() - repeatAttrs->Dict.set("times", "3") - - let repeatProgram: meNode = { - nodeType: Program, - children: Some([ - { - nodeType: Repeat, - children: Some([{nodeType: Say, children: None, attributes: None, content: Some("Hip hip hooray!")}]), - attributes: Some(repeatAttrs), - content: None, - }, - ]), - attributes: None, - content: None, - } - execute(repeatProgram, createMeEnvironment()) - Console.log("") - - // Example 5: Counting - Console.log("Example 5: Counting") - let scoreAttrs = Dict.make() - scoreAttrs->Dict.set("name", "score") - let addAttrs = Dict.make() - addAttrs->Dict.set("to", "score") - let countRepeatAttrs = Dict.make() - countRepeatAttrs->Dict.set("times", "3") - - let countingProgram: meNode = { - nodeType: Program, - children: Some([ - {nodeType: Remember, children: None, attributes: Some(scoreAttrs), content: Some("0")}, - { - nodeType: Repeat, - children: Some([ - {nodeType: Add, children: None, attributes: Some(addAttrs), content: Some("10")}, - {nodeType: Say, children: None, attributes: None, content: Some("Score is now: {score}")}, - ]), - attributes: Some(countRepeatAttrs), - content: None, - }, - ]), - attributes: None, - content: None, - } - execute(countingProgram, createMeEnvironment()) - Console.log("") - - Console.log("=== Demo Complete ===") - Console.log("\nMe makes programming fun and approachable for kids!") -} diff --git a/src/MeParser.affine b/src/MeParser.affine new file mode 100644 index 0000000..3d8742f --- /dev/null +++ b/src/MeParser.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module MeParser; + +// TODO: Complete semantic implementation diff --git a/src/MeParser.res b/src/MeParser.res deleted file mode 100644 index 621e5da..0000000 --- a/src/MeParser.res +++ /dev/null @@ -1,611 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell -// -// Me Language - Parser Module -// -// Parses Me-Dialect source code (XML/HTML-like tags) into the meNode AST. -// Designed for children ages 8-12, so error messages are friendly and -// the parser is intentionally lenient where possible. -// -// ## Supported tags -// -// - text -- Print text (with {var} interpolation) -// - val -- Store a value -// - prompt -- Ask the user for input -// - ... -- Conditional branching -// - ... -- Branch condition -// - ... -- Default branch -// - ... -- Loop n times -// - ... -- Drawing surface -// - -- Draw a shape (self-closing) -// - amount -- Add to a variable -// - amount -- Subtract from a variable -// - -- Stop execution -// -// ## Error handling -// -// Returns Result with friendly error messages that tell -// the child what went wrong and how to fix it. - -// --------------------------------------------------------------------------- -// Parser position tracking -// --------------------------------------------------------------------------- - -/// Tracks where we are in the source string during parsing. -type parserState = { - source: string, - mutable pos: int, -} - -/// Create a new parser state from source code. -let makeState = (source: string): parserState => { - source, - pos: 0, -} - -/// Check whether we have reached the end of the source. -let isAtEnd = (state: parserState): bool => { - state.pos >= state.source->String.length -} - -/// Peek at the current character without advancing. -let peek = (state: parserState): option => { - if isAtEnd(state) { - None - } else { - Some(state.source->String.charAt(state.pos)) - } -} - -/// Advance the position by n characters. -let advance = (state: parserState, n: int): unit => { - state.pos = state.pos + n -} - -/// Check whether the source at the current position starts with a prefix. -let startsWith = (state: parserState, prefix: string): bool => { - let remaining = state.source->String.sliceToEnd(~start=state.pos) - remaining->String.startsWith(prefix) -} - -/// Skip whitespace characters (spaces, tabs, newlines, carriage returns). -let skipWhitespace = (state: parserState): unit => { - let len = state.source->String.length - let continue = ref(true) - while continue.contents && state.pos < len { - let ch = state.source->String.charAt(state.pos) - if ch == " " || ch == "\t" || ch == "\n" || ch == "\r" { - state.pos = state.pos + 1 - } else { - continue := false - } - } -} - -/// Skip an HTML/XML comment: -let skipComment = (state: parserState): unit => { - if startsWith(state, ", just skip to end - if !found.contents { - state.pos = len - } - } -} - -/// Skip all whitespace and comments. -let skipWhitespaceAndComments = (state: parserState): unit => { - let changed = ref(true) - while changed.contents { - let before = state.pos - skipWhitespace(state) - if startsWith(state, " - Hello! - - ` - switch parseOk(source) { - | Some(prog) => - let kids = prog.children->Option.getOr([]) - assertEqual(kids->Array.length, 1, "comments: only 1 real node") && { - let child = kids->Array.getUnsafe(0) - assertEqual(child.content, Some("Hello!"), "comments: correct content") - } - | None => false - } -} - -// --------------------------------------------------------------------------- -// Single-quoted attributes -// --------------------------------------------------------------------------- - -let testParseSingleQuotedAttr = () => { - switch parseOk("blue") { - | Some(prog) => - let kids = prog.children->Option.getOr([]) - let child = kids->Array.getUnsafe(0) - let nameAttr = switch child.attributes { - | Some(attrs) => attrs->Dict.get("name") - | None => None - } - assertEqual(nameAttr, Some("color"), "single-quoted attribute") - | None => false - } -} - -// --------------------------------------------------------------------------- -// Error cases -// --------------------------------------------------------------------------- - -let testErrorUnknownTag = () => { - parseErr("Hello!") -} - -let testErrorUnclosedTag = () => { - parseErr("Hello!") -} - -let testErrorMismatchedClose = () => { - parseErr("Hello!") -} - -let testErrorTextOutsideTag = () => { - parseErr("Hello world") -} - -let testErrorUnclosedAttribute = () => { - parseErr(`Round trip works!") { - | Ok(prog) => - let env = runProgram(prog) - assertEqual(env.output, ["Round trip works!"], "parse+execute: say output") - | Error(_) => false - } -} - -let testParseAndExecuteRememberSay = () => { - let source = ` - cat - I have a {pet}! - ` - switch MeParser.parse(source) { - | Ok(prog) => - let env = runProgram(prog) - assertEqual(env.output, ["I have a cat!"], "parse+execute: interpolation") - | Error(_) => false - } -} - -let testParseAndExecuteRepeat = () => { - let source = ` - - Go! - - ` - switch MeParser.parse(source) { - | Ok(prog) => - let env = runProgram(prog) - assertEqual(env.output, ["Go!", "Go!", "Go!"], "parse+execute: repeat 3x") - | Error(_) => false - } -} - -// --------------------------------------------------------------------------- -// Run all tests -// --------------------------------------------------------------------------- - -let run = (): array => { - [ - runTest("Parser: empty string", testEmptyString), - runTest("Parser: whitespace only", testWhitespaceOnly), - runTest("Parser: basic", testParseSay), - runTest("Parser: with interpolation", testParseSayInterpolation), - runTest("Parser: empty content", testParseSayEmpty), - runTest("Parser: ", testParseRemember), - runTest("Parser: ", testParseAsk), - runTest("Parser: ", testParseAdd), - runTest("Parser: ", testParseSubtract), - runTest("Parser: ", testParseStop), - runTest("Parser: with space", testParseStopWithSpaces), - runTest("Parser: ", testParseRepeat), - runTest("Parser: //", testParseChoose), - runTest("Parser: /", testParseCanvas), - runTest("Parser: multiple top-level nodes", testParseMultiNode), - runTest("Parser: full counting program", testParseCountingProgram), - runTest("Parser: comments are skipped", testParseComments), - runTest("Parser: single-quoted attributes", testParseSingleQuotedAttr), - runTest("Parser: error on unknown tag", testErrorUnknownTag), - runTest("Parser: error on unclosed tag", testErrorUnclosedTag), - runTest("Parser: error on mismatched close", testErrorMismatchedClose), - runTest("Parser: error on text outside tag", testErrorTextOutsideTag), - runTest("Parser: error on unclosed attribute", testErrorUnclosedAttribute), - runTest("Parser: parse+execute ", testParseAndExecuteSay), - runTest("Parser: parse+execute remember+say", testParseAndExecuteRememberSay), - runTest("Parser: parse+execute ", testParseAndExecuteRepeat), - ] -} diff --git a/test/MeTest_Repetition.affine b/test/MeTest_Repetition.affine new file mode 100644 index 0000000..e44a02f --- /dev/null +++ b/test/MeTest_Repetition.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module MeTest_Repetition; + +// TODO: Complete semantic implementation diff --git a/test/MeTest_Repetition.res b/test/MeTest_Repetition.res deleted file mode 100644 index f3847c5..0000000 --- a/test/MeTest_Repetition.res +++ /dev/null @@ -1,214 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2026 hyperpolymath -// -// Me Language Tests: Repetition (repeat with times) -// Covers repeat loops, counting within loops, nested repeats, -// and edge cases like zero iterations. - -open MeTestUtils - -// -- Basic repeat ---------------------------------------------------- - -let testRepeatThreeTimes = () => { - let env = runProgram(program([repeat(3, [say("Go!")])])) - assertEqual(env.output, ["Go!", "Go!", "Go!"], "repeat 3 times") -} - -let testRepeatOnce = () => { - let env = runProgram(program([repeat(1, [say("Once")])])) - assertEqual(env.output, ["Once"], "repeat 1 time") -} - -let testRepeatZeroTimes = () => { - let env = runProgram(program([repeat(0, [say("Never")])])) - assertEqual(env.output, [], "repeat 0 times produces nothing") -} - -// -- Counting inside repeat ------------------------------------------ - -let testRepeatWithCounter = () => { - let env = runProgram( - program([ - remember("count", "0"), - repeat(5, [add("count", "1")]), - say("Count: {count}"), - ]), - ) - assertEqual(env.output, ["Count: 5"], "repeat with counter") -} - -let testRepeatWithAdd = () => { - let env = runProgram( - program([ - remember("score", "0"), - repeat(3, [add("score", "10")]), - ]), - ) - assertEqual( - env.variables->Dict.get("score"), - Some(MeLanguage.Number(30.0)), - "repeat adds 10 three times = 30", - ) -} - -let testRepeatWithSubtract = () => { - let env = runProgram( - program([ - remember("hp", "100"), - repeat(4, [subtract("hp", "15")]), - ]), - ) - assertEqual( - env.variables->Dict.get("hp"), - Some(MeLanguage.Number(40.0)), - "repeat subtracts 15 four times = 40", - ) -} - -// -- Multiple children in repeat ------------------------------------- - -let testRepeatMultipleChildren = () => { - let env = runProgram( - program([ - remember("n", "0"), - repeat(2, [ - add("n", "1"), - say("Step {n}"), - ]), - ]), - ) - assertEqual(env.output, ["Step 1", "Step 2"], "repeat with add and say") -} - -// -- Nested repeat --------------------------------------------------- - -let testNestedRepeat = () => { - let env = runProgram( - program([ - remember("total", "0"), - repeat(3, [ - repeat(2, [add("total", "1")]), - ]), - ]), - ) - assertEqual( - env.variables->Dict.get("total"), - Some(MeLanguage.Number(6.0)), - "nested repeat 3x2 = 6", - ) -} - -// -- Repeat with stop ------------------------------------------------ - -let testRepeatStopsEarly = () => { - let env = runProgram( - program([ - remember("i", "0"), - repeat(10, [ - add("i", "1"), - say("Iteration {i}"), - // Stop after first iteration via a conditional - choose([when_("i", "2", [stop()])]), - ]), - ]), - ) - // Should run iterations 1 and 2, then stop - assertEqual( - env.output, - ["Iteration 1", "Iteration 2"], - "repeat stops early with stop", - ) -} - -// -- Repeat with invalid times --------------------------------------- - -let testRepeatNonNumericTimes = () => { - // Non-numeric times attribute should be ignored - let attrs = Dict.make() - attrs->Dict.set("times", "abc") - let node = makeNode( - ~nodeType=MeLanguage.Repeat, - ~attributes=Some(attrs), - ~children=Some([say("never")]), - (), - ) - let env = runProgram(program([node])) - assertEqual(env.output, [], "repeat with non-numeric times does nothing") -} - -let testRepeatNoTimesAttribute = () => { - // Repeat with no times attribute should do nothing - let node = makeNode( - ~nodeType=MeLanguage.Repeat, - ~children=Some([say("never")]), - (), - ) - let env = runProgram(program([node])) - assertEqual(env.output, [], "repeat without times attribute does nothing") -} - -let testRepeatNoChildren = () => { - // Repeat with times but no children should do nothing - let env = runProgram(program([repeat(5, [])])) - assertEqual(env.output, [], "repeat with no children does nothing") -} - -// -- Large repeat ---------------------------------------------------- - -let testRepeatLarge = () => { - let env = runProgram( - program([ - remember("sum", "0"), - repeat(100, [add("sum", "1")]), - ]), - ) - assertEqual( - env.variables->Dict.get("sum"), - Some(MeLanguage.Number(100.0)), - "repeat 100 times counting", - ) -} - -// -- Repeat with choose inside --------------------------------------- - -let testRepeatWithChoose = () => { - let env = runProgram( - program([ - remember("n", "0"), - repeat(4, [ - add("n", "1"), - choose([ - when_("n", "1", [say("one")]), - when_("n", "2", [say("two")]), - otherwise([say("more")]), - ]), - ]), - ]), - ) - assertEqual( - env.output, - ["one", "two", "more", "more"], - "repeat with choose inside", - ) -} - -// -- Run all tests --------------------------------------------------- - -let run = (): array => { - [ - runTest("Repeat: three times", testRepeatThreeTimes), - runTest("Repeat: once", testRepeatOnce), - runTest("Repeat: zero times", testRepeatZeroTimes), - runTest("Repeat: with counter", testRepeatWithCounter), - runTest("Repeat: with add", testRepeatWithAdd), - runTest("Repeat: with subtract", testRepeatWithSubtract), - runTest("Repeat: multiple children", testRepeatMultipleChildren), - runTest("Repeat: nested", testNestedRepeat), - runTest("Repeat: stops early", testRepeatStopsEarly), - runTest("Repeat: non-numeric times", testRepeatNonNumericTimes), - runTest("Repeat: no times attribute", testRepeatNoTimesAttribute), - runTest("Repeat: no children", testRepeatNoChildren), - runTest("Repeat: large (100)", testRepeatLarge), - runTest("Repeat: with choose inside", testRepeatWithChoose), - ] -} diff --git a/test/MeTest_ValueAndInterpolation.affine b/test/MeTest_ValueAndInterpolation.affine new file mode 100644 index 0000000..500df7f --- /dev/null +++ b/test/MeTest_ValueAndInterpolation.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module MeTest_ValueAndInterpolation; + +// TODO: Complete semantic implementation diff --git a/test/MeTest_ValueAndInterpolation.res b/test/MeTest_ValueAndInterpolation.res deleted file mode 100644 index dc5a572..0000000 --- a/test/MeTest_ValueAndInterpolation.res +++ /dev/null @@ -1,129 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2026 hyperpolymath -// -// Me Language Tests: Value Parsing and String Interpolation -// Covers parseValue, valueToString, and interpolate functions. - -open MeTestUtils - -// -- parseValue ------------------------------------------------------ - -let testParseValueInteger = () => { - let v = MeLanguage.parseValue("42") - assertEqual(v, MeLanguage.Number(42.0), "parseValue integer") -} - -let testParseValueFloat = () => { - let v = MeLanguage.parseValue("3.14") - assertEqual(v, MeLanguage.Number(3.14), "parseValue float") -} - -let testParseValueNegative = () => { - let v = MeLanguage.parseValue("-7") - assertEqual(v, MeLanguage.Number(-7.0), "parseValue negative") -} - -let testParseValueZero = () => { - let v = MeLanguage.parseValue("0") - assertEqual(v, MeLanguage.Number(0.0), "parseValue zero") -} - -let testParseValueString = () => { - let v = MeLanguage.parseValue("hello") - assertEqual(v, MeLanguage.String("hello"), "parseValue non-numeric string") -} - -let testParseValueEmptyString = () => { - let v = MeLanguage.parseValue("") - assertEqual(v, MeLanguage.String(""), "parseValue empty string") -} - -let testParseValueMixedText = () => { - let v = MeLanguage.parseValue("abc123") - assertEqual(v, MeLanguage.String("abc123"), "parseValue mixed text") -} - -// -- valueToString --------------------------------------------------- - -let testValueToStringStr = () => { - let s = MeLanguage.valueToString(MeLanguage.String("hi")) - assertEqual(s, "hi", "valueToString String") -} - -let testValueToStringNum = () => { - let s = MeLanguage.valueToString(MeLanguage.Number(5.0)) - assertEqual(s, "5", "valueToString Number(5)") -} - -// -- interpolate ----------------------------------------------------- - -let testInterpolateSingleVar = () => { - let env = MeLanguage.createMeEnvironment() - env.variables->Dict.set("name", MeLanguage.String("Alex")) - let result = MeLanguage.interpolate("Hello {name}!", env) - assertEqual(result, "Hello Alex!", "interpolate single variable") -} - -let testInterpolateMultipleVars = () => { - let env = MeLanguage.createMeEnvironment() - env.variables->Dict.set("name", MeLanguage.String("Alex")) - env.variables->Dict.set("age", MeLanguage.Number(10.0)) - let result = MeLanguage.interpolate("I am {name}, age {age}.", env) - assertEqual(result, "I am Alex, age 10.", "interpolate multiple variables") -} - -let testInterpolateNoVars = () => { - let env = MeLanguage.createMeEnvironment() - let result = MeLanguage.interpolate("No variables here.", env) - assertEqual(result, "No variables here.", "interpolate no variables") -} - -let testInterpolateUnknownVar = () => { - let env = MeLanguage.createMeEnvironment() - let result = MeLanguage.interpolate("Hello {unknown}!", env) - assertEqual(result, "Hello {unknown}!", "interpolate unknown variable kept") -} - -let testInterpolateAdjacentVars = () => { - let env = MeLanguage.createMeEnvironment() - env.variables->Dict.set("a", MeLanguage.String("X")) - env.variables->Dict.set("b", MeLanguage.String("Y")) - let result = MeLanguage.interpolate("{a}{b}", env) - assertEqual(result, "XY", "interpolate adjacent variables") -} - -let testInterpolateEmptyString = () => { - let env = MeLanguage.createMeEnvironment() - let result = MeLanguage.interpolate("", env) - assertEqual(result, "", "interpolate empty string") -} - -let testInterpolateBracesNoVar = () => { - let env = MeLanguage.createMeEnvironment() - let result = MeLanguage.interpolate("Use {} for fun", env) - // {} contains empty name which won't match any variable - assertEqual(result == "Use {} for fun" || result == "Use for fun", true, "interpolate empty braces") -} - -// -- Run all tests --------------------------------------------------- - -let run = (): array => { - [ - runTest("parseValue: integer", testParseValueInteger), - runTest("parseValue: float", testParseValueFloat), - runTest("parseValue: negative", testParseValueNegative), - runTest("parseValue: zero", testParseValueZero), - runTest("parseValue: string", testParseValueString), - runTest("parseValue: empty string", testParseValueEmptyString), - runTest("parseValue: mixed text", testParseValueMixedText), - runTest("valueToString: String", testValueToStringStr), - runTest("valueToString: Number", testValueToStringNum), - runTest("interpolate: single var", testInterpolateSingleVar), - runTest("interpolate: multiple vars", testInterpolateMultipleVars), - runTest("interpolate: no vars", testInterpolateNoVars), - runTest("interpolate: unknown var", testInterpolateUnknownVar), - runTest("interpolate: adjacent vars", testInterpolateAdjacentVars), - runTest("interpolate: empty string", testInterpolateEmptyString), - runTest("interpolate: empty braces", testInterpolateBracesNoVar), - ] -}