From d5ad853f6b4464f4c7b536fa9a27538decbf3caa Mon Sep 17 00:00:00 2001 From: Nev Offload Date: Fri, 27 Feb 2026 19:55:47 +0200 Subject: [PATCH] test: formalize self-hosted macro runtime specs --- docs/SELF_HOSTED_RUNTIME_SPEC.md | 88 +++++++++ tests/cli.test.ts | 13 +- tests/fixtures/macro/array_macro.ts | 17 ++ tests/fixtures/macro/conditional_macro.ts | 25 +++ .../conditional_macro_already_multiplied.ts | 24 +++ tests/fixtures/macro/debug_sequence_macro.ts | 22 +++ tests/fixtures/macro/introspection_macro.ts | 25 +++ tests/fixtures/macro/member_macro.ts | 17 ++ tests/fixtures/macro/multi_arg_compare.ts | 19 ++ tests/fixtures/macro/object_macro.ts | 17 ++ .../macro/references_introspection.ts | 18 ++ tests/fixtures/macro/references_values.ts | 3 + .../fixtures/macro/variadic_numeric_count.ts | 24 +++ tests/self-hosted/misc.ts | 174 +++++++++++++++++- 14 files changed, 474 insertions(+), 12 deletions(-) create mode 100644 docs/SELF_HOSTED_RUNTIME_SPEC.md create mode 100644 tests/fixtures/macro/array_macro.ts create mode 100644 tests/fixtures/macro/conditional_macro.ts create mode 100644 tests/fixtures/macro/conditional_macro_already_multiplied.ts create mode 100644 tests/fixtures/macro/debug_sequence_macro.ts create mode 100644 tests/fixtures/macro/introspection_macro.ts create mode 100644 tests/fixtures/macro/member_macro.ts create mode 100644 tests/fixtures/macro/multi_arg_compare.ts create mode 100644 tests/fixtures/macro/object_macro.ts create mode 100644 tests/fixtures/macro/references_introspection.ts create mode 100644 tests/fixtures/macro/references_values.ts create mode 100644 tests/fixtures/macro/variadic_numeric_count.ts diff --git a/docs/SELF_HOSTED_RUNTIME_SPEC.md b/docs/SELF_HOSTED_RUNTIME_SPEC.md new file mode 100644 index 0000000..8458208 --- /dev/null +++ b/docs/SELF_HOSTED_RUNTIME_SPEC.md @@ -0,0 +1,88 @@ +# Self-Hosted Runtime Spec + +This document defines the runtime spec using self-hosted tests as the source of truth. + +The spec is exercised by scenarios in: + +- `/tests/self-hosted/basic.ts` +- `/tests/self-hosted/misc.ts` +- `/tests/self-hosted/http.ts` +- `/tests/self-hosted/stdlib.ts` + +For macro runtime behavior, the normative requirements below are anchored in +`/tests/self-hosted/misc.ts` and macro fixtures under `/tests/fixtures/macro`. + +## Macro Runtime Requirements + +1. `SPEC-MACRO-RUNTIME-001` +- Requirement: `Closure()` MUST normalize plain-object references into a `Map`. +- Scenario: `macro :: [SPEC-MACRO-RUNTIME-001] ...` + +2. `SPEC-MACRO-RUNTIME-002` +- Requirement: `Closure()` MUST preserve `Map` references as `Map`. +- Scenario: `macro :: [SPEC-MACRO-RUNTIME-002] ...` + +3. `SPEC-MACRO-RUNTIME-003` +- Requirement: Canonical reference records MUST expose `uri` and `name`. +- Scenario: `macro :: [SPEC-MACRO-RUNTIME-003] ...` + +4. `SPEC-MACRO-RUNTIME-004` +- Requirement: `Definition()` MUST produce `declaration` and `references`. +- Scenario: `macro :: [SPEC-MACRO-RUNTIME-004] ...` + +5. `SPEC-MACRO-EXPANSION-001` +- Requirement: `createMacro`-marked closures MUST expand at bundle time. +- Scenario: `macro :: [SPEC-MACRO-EXPANSION-001] ...` +- Fixture: `/tests/fixtures/macro/closure-macro.ts` + +6. `SPEC-MACRO-REFERENCES-001` +- Requirement: Cross-file identifiers used in captured closures MUST be tracked in references. +- Scenario: `macro :: [SPEC-MACRO-REFERENCES-001] ...` +- Fixture: `/tests/fixtures/macro/cross-file-ref/entry.ts` + +7. `SPEC-MACRO-EXEC-001` +- Requirement: Macro execution MUST support conditional branching based on captured expression shape. +- Scenario: `macro :: [SPEC-MACRO-EXEC-001] ...` +- Fixture: `/tests/fixtures/macro/conditional_macro.ts` + +8. `SPEC-MACRO-EXEC-002` +- Requirement: Macro execution MUST expose `arg.expression` for compile-time introspection. +- Scenario: `macro :: [SPEC-MACRO-EXEC-002] ...` +- Fixture: `/tests/fixtures/macro/introspection_macro.ts` + +9. `SPEC-MACRO-EXEC-003` +- Requirement: Macro execution MUST pass multiple closure arguments in order. +- Scenario: `macro :: [SPEC-MACRO-EXEC-003] ...` +- Fixture: `/tests/fixtures/macro/multi_arg_compare.ts` + +10. `SPEC-MACRO-EXEC-004` +- Requirement: Variadic macro signatures MUST receive all captured arguments. +- Scenario: `macro :: [SPEC-MACRO-EXEC-004] ...` +- Fixture: `/tests/fixtures/macro/variadic_numeric_count.ts` + +11. `SPEC-MACRO-REFERENCES-002` +- Requirement: Macro execution MUST expose `arg.references` for compile-time inspection. +- Scenario: `macro :: [SPEC-MACRO-REFERENCES-002] ...` +- Fixture: `/tests/fixtures/macro/references_introspection.ts` + +12. `SPEC-MACRO-EXEC-005` +- Requirement: Macro output MUST support object/array/member-expression code generation. +- Scenario: `macro :: [SPEC-MACRO-EXEC-005] ...` +- Fixture: `/tests/fixtures/macro/object_array_member_macro.ts` + +13. `SPEC-MACRO-EXEC-006` +- Requirement: Macro output MUST support sequence-expression style emitted code. +- Scenario: `macro :: [SPEC-MACRO-EXEC-006] ...` +- Fixture: `/tests/fixtures/macro/debug_sequence_macro.ts` + +## CLI Conformance Backstops + +Integration-level conformance remains covered in `/tests/cli.test.ts`: + +- Macro detection and expansion flow +- Macro-added references +- Recursive expansion +- Infinite recursion guard +- Emitted output does not retain macro definitions + +Self-hosted scenarios are normative; CLI tests are regression backstops. diff --git a/tests/cli.test.ts b/tests/cli.test.ts index e2a0ebf..840166c 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -348,8 +348,11 @@ describe('funee CLI', () => { * * The Closure should capture both the mult expression AND the multiplier reference. */ - // TODO: Create fixture and implement this test - expect(true).toBe(true); // Placeholder + const { stdout, exitCode } = await runFunee(['macro/cross-file-ref/entry.ts']); + + expect(exitCode).toBe(0); + expect(stdout).toContain("captured.references is Map: true"); + expect(stdout).toContain("has 'add' reference: true"); }); it('expands closure macro at bundle time', async () => { @@ -375,8 +378,10 @@ describe('funee CLI', () => { * When capturing an expression that references external declarations, * the Closure should include those in its references map */ - // TODO: Test with expression that has external refs - expect(true).toBe(true); + const { stdout, exitCode } = await runFunee(['macro/references_introspection.ts']); + + expect(exitCode).toBe(0); + expect(stdout).toContain("references:has_someFunc=1"); }); // ===== STEP 3: MACRO EXECUTION TESTS ===== diff --git a/tests/fixtures/macro/array_macro.ts b/tests/fixtures/macro/array_macro.ts new file mode 100644 index 0000000..72deae2 --- /dev/null +++ b/tests/fixtures/macro/array_macro.ts @@ -0,0 +1,17 @@ +import { log } from "funee"; + +const createMacro = (fn: any) => fn; + +const makeArray = createMacro((...args: any[]) => { + return { + expression: `[${args.map((arg) => arg.expression).join(", ")}]`, + references: new Map(), + }; +}); + +const arr = makeArray(1, 2, 3); + +export default () => { + log(`array:first=${arr[0]}`); + log(`array:third=${arr[2]}`); +}; diff --git a/tests/fixtures/macro/conditional_macro.ts b/tests/fixtures/macro/conditional_macro.ts new file mode 100644 index 0000000..146685c --- /dev/null +++ b/tests/fixtures/macro/conditional_macro.ts @@ -0,0 +1,25 @@ +import { log } from "funee"; + +const createMacro = (fn: any) => fn; + +// If expression is already a multiplication, leave it unchanged. +const smartDouble = createMacro((arg: any) => { + const expr = String(arg.expression).trim(); + if (expr.includes("*")) { + return { + expression: expr, + references: arg.references, + }; + } + + return { + expression: `(${expr}) * 2`, + references: arg.references, + }; +}); + +const result = smartDouble(5); + +export default () => { + log(`conditional:result=${result}`); +}; diff --git a/tests/fixtures/macro/conditional_macro_already_multiplied.ts b/tests/fixtures/macro/conditional_macro_already_multiplied.ts new file mode 100644 index 0000000..5564919 --- /dev/null +++ b/tests/fixtures/macro/conditional_macro_already_multiplied.ts @@ -0,0 +1,24 @@ +import { log } from "funee"; + +const createMacro = (fn: any) => fn; + +const smartDouble = createMacro((arg: any) => { + const expr = String(arg.expression).trim(); + if (expr.includes("*")) { + return { + expression: expr, + references: arg.references, + }; + } + + return { + expression: `(${expr}) * 2`, + references: arg.references, + }; +}); + +const result = smartDouble(5 * 2); + +export default () => { + log(`conditional_already_multiplied:result=${result}`); +}; diff --git a/tests/fixtures/macro/debug_sequence_macro.ts b/tests/fixtures/macro/debug_sequence_macro.ts new file mode 100644 index 0000000..0963044 --- /dev/null +++ b/tests/fixtures/macro/debug_sequence_macro.ts @@ -0,0 +1,22 @@ +import { log } from "funee"; + +const createMacro = (fn: any) => fn; + +const debug = createMacro((arg: any) => { + const exprType = String(arg.expression).includes("+") + ? "BinaryExpression" + : "Expression"; + + return { + expression: `(log("[DEBUG] Expression type: ${exprType}"), (${arg.expression}))`, + references: arg.references, + }; +}); + +const x = 5; +const y = 10; +const result = debug(x + y); + +export default () => { + log(`debug:result=${result}`); +}; diff --git a/tests/fixtures/macro/introspection_macro.ts b/tests/fixtures/macro/introspection_macro.ts new file mode 100644 index 0000000..21f9e9e --- /dev/null +++ b/tests/fixtures/macro/introspection_macro.ts @@ -0,0 +1,25 @@ +import { log } from "funee"; + +const createMacro = (fn: any) => fn; + +const getExprType = createMacro((arg: any) => { + const expr = String(arg.expression).trim(); + + let exprType = "Unknown"; + if (/^-?\d+(\.\d+)?$/.test(expr)) { + exprType = "NumericLiteral"; + } else if (/[+\-*/]/.test(expr)) { + exprType = "BinaryExpression"; + } + + return { + expression: `"${exprType}"`, + references: new Map(), + }; +}); + +const type = getExprType(5 + 3); + +export default () => { + log(`introspection:type=${type}`); +}; diff --git a/tests/fixtures/macro/member_macro.ts b/tests/fixtures/macro/member_macro.ts new file mode 100644 index 0000000..2c156d9 --- /dev/null +++ b/tests/fixtures/macro/member_macro.ts @@ -0,0 +1,17 @@ +import { log } from "funee"; + +const createMacro = (fn: any) => fn; + +const getProperty = createMacro((objArg: any, propArg: any) => { + return { + expression: `(${objArg.expression})[${propArg.expression}]`, + references: objArg.references, + }; +}); + +const obj = { value: 42, name: "test" }; +const value = getProperty(obj, "value"); + +export default () => { + log(`member:value=${value}`); +}; diff --git a/tests/fixtures/macro/multi_arg_compare.ts b/tests/fixtures/macro/multi_arg_compare.ts new file mode 100644 index 0000000..2025812 --- /dev/null +++ b/tests/fixtures/macro/multi_arg_compare.ts @@ -0,0 +1,19 @@ +import { log } from "funee"; + +const createMacro = (fn: any) => fn; + +const assertEqual = createMacro((expected: any, actual: any) => { + const left = String(expected.expression).trim(); + const right = String(actual.expression).trim(); + + return { + expression: left === right ? "1" : "0", + references: new Map(), + }; +}); + +const result = assertEqual(5, 5); + +export default () => { + log(`multiarg:result=${result}`); +}; diff --git a/tests/fixtures/macro/object_macro.ts b/tests/fixtures/macro/object_macro.ts new file mode 100644 index 0000000..00d4c92 --- /dev/null +++ b/tests/fixtures/macro/object_macro.ts @@ -0,0 +1,17 @@ +import { log } from "funee"; + +const createMacro = (fn: any) => fn; + +const makeConfig = createMacro((nameArg: any, valueArg: any) => { + return { + expression: `({ name: ${nameArg.expression}, value: ${valueArg.expression} })`, + references: new Map(), + }; +}); + +const config = makeConfig("test", 42); + +export default () => { + log(`object:name=${config.name}`); + log(`object:value=${config.value}`); +}; diff --git a/tests/fixtures/macro/references_introspection.ts b/tests/fixtures/macro/references_introspection.ts new file mode 100644 index 0000000..056fae8 --- /dev/null +++ b/tests/fixtures/macro/references_introspection.ts @@ -0,0 +1,18 @@ +import { log } from "funee"; +import { someFunc } from "./references_values.ts"; + +const createMacro = (fn: any) => fn; + +const checkHasReference = createMacro((arg: any) => { + const hasRef = arg.references.has("someFunc") ? 1 : 0; + return { + expression: `${hasRef}`, + references: new Map(), + }; +}); + +const result = checkHasReference(someFunc()); + +export default () => { + log(`references:has_someFunc=${result}`); +}; diff --git a/tests/fixtures/macro/references_values.ts b/tests/fixtures/macro/references_values.ts new file mode 100644 index 0000000..39d21c9 --- /dev/null +++ b/tests/fixtures/macro/references_values.ts @@ -0,0 +1,3 @@ +export function someFunc() { + return 42; +} diff --git a/tests/fixtures/macro/variadic_numeric_count.ts b/tests/fixtures/macro/variadic_numeric_count.ts new file mode 100644 index 0000000..4340a59 --- /dev/null +++ b/tests/fixtures/macro/variadic_numeric_count.ts @@ -0,0 +1,24 @@ +import { log } from "funee"; + +const createMacro = (fn: any) => fn; + +const countNumericArgs = createMacro((...args: any[]) => { + let count = 0; + for (const arg of args) { + const expr = String(arg.expression).trim(); + if (/^-?\d+(\.\d+)?$/.test(expr)) { + count++; + } + } + + return { + expression: `${count}`, + references: new Map(), + }; +}); + +const count = countNumericArgs(5, "hello", 10); + +export default () => { + log(`variadic:count=${count}`); +}; diff --git a/tests/self-hosted/misc.ts b/tests/self-hosted/misc.ts index 9d06f04..4e88576 100644 --- a/tests/self-hosted/misc.ts +++ b/tests/self-hosted/misc.ts @@ -28,6 +28,8 @@ import { watchDirectory, } from "funee"; +const FUNEE = "./target/release/funee"; + // ============================================================================ // SUBPROCESS SCENARIOS // ============================================================================ @@ -292,7 +294,8 @@ const timerScenarios = [ const macroScenarios = [ // Closure constructor accepts plain objects scenario({ - description: "macro :: Closure constructor converts plain objects to Map", + description: + "macro :: [SPEC-MACRO-RUNTIME-001] Closure constructor converts plain objects to Map", verify: { expression: async () => { const c = Closure({ @@ -310,7 +313,8 @@ const macroScenarios = [ // Closure constructor accepts Map scenario({ - description: "macro :: Closure constructor accepts Map references", + description: + "macro :: [SPEC-MACRO-RUNTIME-002] Closure constructor accepts Map references", verify: { expression: async () => { const refsMap = new Map([ @@ -332,7 +336,8 @@ const macroScenarios = [ // CanonicalName type structure scenario({ - description: "macro :: CanonicalName has uri and name properties", + description: + "macro :: [SPEC-MACRO-RUNTIME-003] CanonicalName has uri and name properties", verify: { expression: async () => { // CanonicalName is a structural type { uri, name } @@ -351,7 +356,8 @@ const macroScenarios = [ // Definition type structure scenario({ - description: "macro :: Definition has declaration and references", + description: + "macro :: [SPEC-MACRO-RUNTIME-004] Definition has declaration and references", verify: { expression: async () => { const def = Definition({ @@ -368,10 +374,13 @@ const macroScenarios = [ // Test actual closure macro expansion via fixture scenario({ - description: "macro :: closure macro expands to AST at compile time", + description: + "macro :: [SPEC-MACRO-EXPANSION-001] closure macro expands to AST at compile time", verify: { expression: async () => { - const result = await spawn("./target/release/funee", ["tests/fixtures/macro/closure-macro.ts"]); + const result = await spawn(FUNEE, [ + "tests/fixtures/macro/closure-macro.ts", + ]); await assertThat(result.status.code, is(0)); await assertThat(result.stdoutText(), contains("type: object")); await assertThat(result.stdoutText(), contains("AST type: ArrowFunctionExpression")); @@ -382,11 +391,160 @@ const macroScenarios = [ // Test macro with cross-file references scenario({ - description: "macro :: closure captures cross-file references", + description: + "macro :: [SPEC-MACRO-REFERENCES-001] closure captures cross-file references", verify: { expression: async () => { - const result = await spawn("./target/release/funee", ["tests/fixtures/macro/cross-file-ref/entry.ts"]); + const result = await spawn(FUNEE, [ + "tests/fixtures/macro/cross-file-ref/entry.ts", + ]); await assertThat(result.status.code, is(0)); + await assertThat(result.stdoutText(), contains("has 'add' reference: true")); + }, + references: new Map(), + } as Closure<() => Promise>, + }), + + // Conditional macro expansion + scenario({ + description: + "macro :: [SPEC-MACRO-EXEC-001] conditional macro branches on expression shape", + verify: { + expression: async () => { + const first = await spawn(FUNEE, [ + "tests/fixtures/macro/conditional_macro.ts", + ]); + await assertThat(first.status.code, is(0)); + await assertThat(first.stdoutText(), contains("conditional:result=10")); + + const second = await spawn(FUNEE, [ + "tests/fixtures/macro/conditional_macro_already_multiplied.ts", + ]); + await assertThat(second.status.code, is(0)); + await assertThat( + second.stdoutText(), + contains("conditional_already_multiplied:result=10") + ); + }, + references: new Map(), + } as Closure<() => Promise>, + }), + + // Introspection over captured expression + scenario({ + description: + "macro :: [SPEC-MACRO-EXEC-002] macro can inspect arg.expression", + verify: { + expression: async () => { + const result = await spawn(FUNEE, [ + "tests/fixtures/macro/introspection_macro.ts", + ]); + await assertThat(result.status.code, is(0)); + await assertThat( + result.stdoutText(), + contains("introspection:type=BinaryExpression") + ); + }, + references: new Map(), + } as Closure<() => Promise>, + }), + + // Two-argument macro behavior + scenario({ + description: + "macro :: [SPEC-MACRO-EXEC-003] two-argument macro receives both Closure arguments", + verify: { + expression: async () => { + const result = await spawn(FUNEE, [ + "tests/fixtures/macro/multi_arg_compare.ts", + ]); + await assertThat(result.status.code, is(0)); + await assertThat(result.stdoutText(), contains("multiarg:result=1")); + }, + references: new Map(), + } as Closure<() => Promise>, + }), + + // Variadic macro behavior + scenario({ + description: + "macro :: [SPEC-MACRO-EXEC-004] variadic macros receive all arguments", + verify: { + expression: async () => { + const result = await spawn(FUNEE, [ + "tests/fixtures/macro/variadic_numeric_count.ts", + ]); + await assertThat(result.status.code, is(0)); + await assertThat(result.stdoutText(), contains("variadic:count=2")); + }, + references: new Map(), + } as Closure<() => Promise>, + }), + + // References-map introspection + scenario({ + description: + "macro :: [SPEC-MACRO-REFERENCES-002] macro can inspect arg.references", + verify: { + expression: async () => { + const result = await spawn(FUNEE, [ + "tests/fixtures/macro/references_introspection.ts", + ]); + await assertThat(result.status.code, is(0)); + await assertThat( + result.stdoutText(), + contains("references:has_someFunc=1") + ); + }, + references: new Map(), + } as Closure<() => Promise>, + }), + + // Object/Array/Member transformations from macro output + scenario({ + description: + "macro :: [SPEC-MACRO-EXEC-005] macro output can construct object/array/member expressions", + verify: { + expression: async () => { + const objectResult = await spawn(FUNEE, [ + "tests/fixtures/macro/object_macro.ts", + ]); + await assertThat(objectResult.status.code, is(0)); + await assertThat(objectResult.stdoutText(), contains("object:name=test")); + await assertThat(objectResult.stdoutText(), contains("object:value=42")); + + const arrayResult = await spawn(FUNEE, [ + "tests/fixtures/macro/array_macro.ts", + ]); + await assertThat(arrayResult.status.code, is(0)); + await assertThat(arrayResult.stdoutText(), contains("array:first=1")); + await assertThat(arrayResult.stdoutText(), contains("array:third=3")); + + const memberResult = await spawn(FUNEE, [ + "tests/fixtures/macro/member_macro.ts", + ]); + await assertThat(memberResult.status.code, is(0)); + await assertThat(memberResult.stdoutText(), contains("member:value=42")); + }, + references: new Map(), + } as Closure<() => Promise>, + }), + + // Sequence-expression style debug macro + scenario({ + description: + "macro :: [SPEC-MACRO-EXEC-006] macro output supports sequence-expression evaluation", + verify: { + expression: async () => { + const result = await spawn(FUNEE, [ + "tests/fixtures/macro/debug_sequence_macro.ts", + ]); + await assertThat(result.status.code, is(0)); + await assertThat( + result.stdoutText(), + contains("[DEBUG] Expression type: BinaryExpression") + ); + await assertThat(result.stdoutText(), contains("debug:result=15")); }, references: new Map(), } as Closure<() => Promise>,