Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions docs/SELF_HOSTED_RUNTIME_SPEC.md
Original file line number Diff line number Diff line change
@@ -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.
13 changes: 9 additions & 4 deletions tests/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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 =====
Expand Down
17 changes: 17 additions & 0 deletions tests/fixtures/macro/array_macro.ts
Original file line number Diff line number Diff line change
@@ -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]}`);
};
25 changes: 25 additions & 0 deletions tests/fixtures/macro/conditional_macro.ts
Original file line number Diff line number Diff line change
@@ -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}`);
};
24 changes: 24 additions & 0 deletions tests/fixtures/macro/conditional_macro_already_multiplied.ts
Original file line number Diff line number Diff line change
@@ -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}`);
};
22 changes: 22 additions & 0 deletions tests/fixtures/macro/debug_sequence_macro.ts
Original file line number Diff line number Diff line change
@@ -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}`);
};
25 changes: 25 additions & 0 deletions tests/fixtures/macro/introspection_macro.ts
Original file line number Diff line number Diff line change
@@ -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}`);
};
17 changes: 17 additions & 0 deletions tests/fixtures/macro/member_macro.ts
Original file line number Diff line number Diff line change
@@ -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}`);
};
19 changes: 19 additions & 0 deletions tests/fixtures/macro/multi_arg_compare.ts
Original file line number Diff line number Diff line change
@@ -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}`);
};
17 changes: 17 additions & 0 deletions tests/fixtures/macro/object_macro.ts
Original file line number Diff line number Diff line change
@@ -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}`);
};
18 changes: 18 additions & 0 deletions tests/fixtures/macro/references_introspection.ts
Original file line number Diff line number Diff line change
@@ -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}`);
};
3 changes: 3 additions & 0 deletions tests/fixtures/macro/references_values.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function someFunc() {
return 42;
}
24 changes: 24 additions & 0 deletions tests/fixtures/macro/variadic_numeric_count.ts
Original file line number Diff line number Diff line change
@@ -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}`);
};
Loading