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
63 changes: 53 additions & 10 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -906,7 +906,7 @@ Added comprehensive input validation to all Scheme procedures.
## Special Forms ([analyzer.js](./src/core/interpreter/analyzer.js))

- `analyzeIf` - Validates 2-3 arguments
- `analyzeLet` - Validates binding structure
- `analyzeLet` - Validates binding structure
- `analyzeLetRec` - Validates binding structure
- `analyzeLambda` - Validates param symbols, body not empty
- `analyzeSet` - Validates symbol argument
Expand Down Expand Up @@ -1171,7 +1171,7 @@ Updated [base.sld](/workspaces/scheme-js-4/src/core/scheme/base.sld):
char? char=? char<? char>? char<=? char>=?
char->integer integer->char

;; Strings
;; Strings
string? make-string string string-length string-ref
string=? string<? string>? string<=? string>=?
substring string-append string-copy
Expand Down Expand Up @@ -1379,7 +1379,7 @@ This was the most complex part of the implementation, solving the problem of int

- **Problem**: Internal library definitions like `param-dynamic-bind` (used by the `parameterize` macro in `scheme core`) were defined in library-specific environments but `GlobalRef` only looked in the global environment.

- **Solution**:
- **Solution**:
- Introduced `libraryScopeEnvMap` in `syntax_object.js` to map library defining scopes to their runtime environments.
- Updated `library_loader.js` to register this mapping when loading libraries via `registerLibraryScope(libraryScope, libEnv)`.
- Updated `GlobalRef` to carry the defining scope ID.
Expand All @@ -1399,7 +1399,7 @@ This was the most complex part of the implementation, solving the problem of int
### Hygiene Tests (`tests/functional/macro_tests.js`)
All 8 tests pass:
- ✅ **Standard Library Capture**: `(syntax-rules () ((_) (list 1 2)))` works even if `list` is shadowed locally
- ✅ **User Global Capture**: `(syntax-rules () ((_) global-var))` works even if `global-var` is shadowed locally
- ✅ **User Global Capture**: `(syntax-rules () ((_) global-var))` works even if `global-var` is shadowed locally
- ✅ **Renaming**: Macro-introduced variables don't clash with user variables

### Regression Tests
Expand Down Expand Up @@ -1500,7 +1500,7 @@ The fix was verified using a targeted reproduction test case and the full system
(guard (e (else (if (string? e) e "not-string")))
expr))))

(test "error" (test-guard-binding (raise "error")))
(test "error" (test-guard-binding (raise "error")))
;; Previously failed with "Unbound variable: e" or "global"
;; Now correctly returns "error"

Expand Down Expand Up @@ -1574,7 +1574,7 @@ Changed macro lookup to use scoped registry:
## Tests Added
Created [hygiene_tests.scm](./tests/core/scheme/hygiene_tests.scm) with 10 tests:
- Referential transparency (3 tests)
- let-syntax scoping (4 tests)
- let-syntax scoping (4 tests)
- letrec-syntax (2 tests)
- Hygiene edge cases (1 test)

Expand Down Expand Up @@ -2151,7 +2151,7 @@ I have implemented a robust Node.js REPL application for the Scheme interpreter

- **Interactive REPL**: Run `node repl.js` to start the session.
- **Multi-line Input**: The REPL detects incomplete expressions (open parentheses, unclosed strings) and prompts for continuation.
- **File Loading**: Use `(load "filename.scm")` to load Scheme scripts into the current environment.
- **File Loading**: Use `(load "filename.scm")` to load Scheme scripts into the current environment.
- **Library Imports**: Use `(import (lib name))` to import R7RS libraries synchronously.
- **File Execution**: Run `node repl.js <file.scm>` to execute a Scheme file.
- **Expression Evaluation**: Run `node repl.js -e "<expr>"` to evaluate a single expression and exit.
Expand Down Expand Up @@ -2314,7 +2314,7 @@ TEST SUMMARY: 1166 passed, 0 failed, 3 skipped
(js-promise-then p (lambda (x) (display x)))

;; Create promise with executor
(define p2 (make-js-promise
(define p2 (make-js-promise
(lambda (resolve reject)
(resolve (* 6 7)))))

Expand Down Expand Up @@ -3379,7 +3379,7 @@ Implemented proper object printing with reader syntax `#{(key val)...}` and circ
### 2. Reader Bridge Fixes (`src/core/primitives/io/reader_bridge.js`)
- Added `braceDepth` tracking for `{` and `}` to allow the reader to correctly collect full object literal "chunks" from ports.
- Added explicit handling for the `#{` token start.
- **Bug Fixes**:
- **Bug Fixes**:
- Fixed an issue where strings ending inside an object literal (e.g., `#{(a \"s\")}`) caused the reader to break early.
- Fixed an issue where whitespace following a nested list in an object literal (e.g., `#{(a 1) (b 2)}`) caused premature parsing.

Expand Down Expand Up @@ -3888,7 +3888,7 @@ Integrated the debugging runtime into the interactive REPLs, providing a profess

## Documentation

- **[debugger_manual.md](./docs/debugger_manual.md)**: Created a detailed user manual for all debugger commands
- **[debugger_manual.md](./docs/debugger_manual.md)**: Created a detailed user manual for all debugger commands
and features.

## Verification Results
Expand Down Expand Up @@ -4024,3 +4024,46 @@ I converted the REPL's evaluation logic to be fully asynchronous in debug mode.

### Node.js REPL
- Verified that `:abort` exits the debug loop and returns to the main prompt.

# Walkthrough: Implementing define-macro (2026-02-08)

I have implemented the `define-macro` special form, bringing Common Lisp-style unhygienic macros to Scheme-JS.

## Changes

### 1. Special Form Handler
- **`src/core/interpreter/analyzers/core_forms.js`**: Added `analyzeDefineMacro`.
- It creates a temporary `Interpreter` with a fresh standard environment.
- It evaluates the transformer body (which is regular Scheme code) into a closure.
- It registers a JS wrapper function in the macro registry that invokes this closure during expansion.
- **`src/core/interpreter/library_registry.js`**: Added `define-macro` to `SYNTAX_KEYWORDS` and `SPECIAL_FORMS`.

### 2. Library Support
- **`src/extras/scheme/define-macro.sld`**: Created a library exporting `define-macro` under the `scheme-js` namespace.
- **`repl.js` & `web/main.js`**: Updated bootstrap logic to import `(scheme-js define-macro)` by default.
- **`tests/run_scheme_tests_lib.js`**: Updated test runner to include `(scheme-js define-macro)`.

### 3. Verification
- Created `tests/functional/test_defmacro.scm` demonstrating:
- Standard list destructuring: `(define-macro (name . args) ...)`
- Explicit transformer: `(define-macro name transformer-proc)`
- Usage in expressions.
- Added to `tests/test_manifest.js`.

## Verification Results

### Automated Tests
Ran the full test suite.

```
TEST SUMMARY: 2021 passed, 0 failed, 7 skipped
```

### Manual Verification
Ran `node repl.js tests/functional/test_defmacro.scm`.

```
OK
Math OK
Unless OK
```
3 changes: 2 additions & 1 deletion repl.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,8 @@ async function bootstrapInterpreter() {
(scheme file)
(scheme process-context)
(scheme-js promise)
(scheme-js interop))
(scheme-js interop)
(scheme-js define-macro))
`;
for (const sexp of parse(imports)) {
interpreter.run(analyze(sexp), env, [], undefined, { jsAutoConvert: 'raw' });
Expand Down
107 changes: 107 additions & 0 deletions src/core/interpreter/analyzers/core_forms.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import { MacroRegistry } from '../macro_registry.js';
import { registerHandler } from './registry.js';
import { compileSyntaxRules } from '../syntax_rules.js';
import { SchemeSyntaxError } from '../errors.js';
import { Interpreter } from '../interpreter.js';
import { createGlobalEnvironment } from '../../primitives/index.js';

// These will be set by the analyzer when it initializes
// to avoid circular dependencies between analyzer.js and core_forms.js.
Expand Down Expand Up @@ -464,6 +466,7 @@ export function registerCoreForms() {
registerHandler('define', analyzeDefine);
registerHandler('begin', analyzeBegin);
registerHandler('define-syntax', analyzeDefineSyntax);
registerHandler('define-macro', analyzeDefineMacro);
registerHandler('let-syntax', analyzeLetSyntax);
registerHandler('letrec-syntax', analyzeLetrecSyntax);
registerHandler('quasiquote', (exp, env, ctx) => expandQuasiquote(cadr(exp), env, ctx));
Expand Down Expand Up @@ -542,6 +545,110 @@ function analyzeDefineSyntax(exp, syntacticEnv = null, ctx) {
return new LiteralNode(null);
}

/**
* Analyzes (define-macro (name args...) body...) or (define-macro name transformer).
* Evaluates the transformer immediately in a fresh environment and registers it.
*
* @param {Cons} exp - The expression.
* @param {SyntacticEnv} [syntacticEnv=null] - The environment.
* @param {InterpreterContext} ctx - The context.
* @returns {LiteralNode}
*/
function analyzeDefineMacro(exp, syntacticEnv = null, ctx) {
const head = cadr(exp);
let name;
let transformerLambdaAst;

// Case 1: (define-macro (name args...) body...)
if (head instanceof Cons) {
const nameObj = car(head);
const args = cdr(head);
const body = cddr(exp);
name = (nameObj instanceof Symbol) ? nameObj.name : syntaxName(nameObj);

// Construct lambda: (lambda args body...)
const lambdaExp = cons(intern('lambda'), cons(args, body));
transformerLambdaAst = analyzeLambda(lambdaExp, syntacticEnv, ctx);
}
// Case 2: (define-macro name transformer-proc)
else if (head instanceof Symbol || isSyntaxObject(head)) {
name = (head instanceof Symbol) ? head.name : syntaxName(head);
const transformerExp = caddr(exp);
transformerLambdaAst = analyze(transformerExp, syntacticEnv, ctx);
} else {
throw new SchemeSyntaxError('Invalid define-macro syntax', exp, 'define-macro');
}

// 1. Create a temporary interpreter with standard primitives
// We use a separate context for the expansion environment to avoid polluting
// the global state, but we might want to share state in the future.
// For now, "expansion time" is a fresh environment with standard library.
const expansionInterpreter = new Interpreter(ctx);
const expansionEnv = createGlobalEnvironment(expansionInterpreter);
expansionInterpreter.setGlobalEnv(expansionEnv);

// 2. Evaluate the transformer to get a closure
let transformerClosure;
try {
// Run synchronously - transformers must be available immediately
transformerClosure = expansionInterpreter.run(transformerLambdaAst, expansionEnv);
} catch (e) {
throw new SchemeSyntaxError(`Error evaluating macro transformer for '${name}': ${e.message}`, exp, 'define-macro');
}

// 3. Create the JS transformer wrapper
// The wrapper receives the macro call expression (name arg1 arg2 ...)
// It extracts the arguments and calls the Scheme closure.
const jsTransformer = (macroCallExp, useSiteEnv) => {
const argsList = cdr(macroCallExp); // (arg1 arg2 ...)

// Invoke the closure with the arguments list
// Note: The transformer returns a Scheme expression (AST/Cons)
// which the analyzer will then recursively analyze.

// We use runWithSentinel to ensure proper stack handling if the macro calls back into JS (unlikely but possible)
// But for simple closure invocation, we can construct an application AST.

// However, we have a raw closure object and raw arguments (Cons list).
// The simplest way is to use the interpreter's invokeContinuation-like logic or apply primitive logic.

// Let's manually construct an Apply invocation or similar.
// Or simpler: use expansionInterpreter.run with a TailAppNode if we wrap the closure in a LiteralNode.

// We need to convert the Cons list of args into an array of AST nodes?
// No, the closure expects Scheme values (Cons list of syntax).
// Wait, (define-macro (f x) ...) expects x to be passed as argument.
// The macro call is (f arg1).
// So the transformer should be called with `arg1`.

// If the macro is (define-macro (f . args) ...), it expects `args` as a list.
// If the macro is (define-macro (f x) ...), it expects `x`.

// We need to `apply` the closure to the arguments list.
// The `apply` primitive in Scheme does exactly this.

try {
// Use the expansion interpreter to run (apply closure argsList)
const applyNode = new TailAppNode(
new VariableNode('apply'),
[
new LiteralNode(transformerClosure),
new LiteralNode(argsList)
]
);

return expansionInterpreter.run(applyNode, expansionEnv);
} catch (e) {
throw new SchemeSyntaxError(`Error expanding macro '${name}': ${e.message}`, macroCallExp, name);
}
};

// 4. Register the transformer
ctx.currentMacroRegistry.define(name, jsTransformer);

return new LiteralNode(null);
}

/**
* Analyzes (let-syntax (<binding>...) <body>).
* @param {Cons} exp - The expression.
Expand Down
5 changes: 3 additions & 2 deletions src/core/interpreter/library_registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,8 @@ export const SYNTAX_KEYWORDS = new Set([
'define-syntax', 'let-syntax', 'letrec-syntax',
'syntax-rules', '...', 'else', '=>', 'import', 'export',
'define-library', 'include', 'include-ci', 'include-library-declarations',
'cond-expand', 'let', 'letrec', 'call/cc', 'call-with-current-continuation'
'cond-expand', 'let', 'letrec', 'call/cc', 'call-with-current-continuation',
'define-macro'
]);

/**
Expand All @@ -266,7 +267,7 @@ export const SPECIAL_FORMS = new Set([
'if', 'let', 'letrec', 'lambda', 'set!', 'define', 'begin',
'quote', 'quasiquote', 'unquote', 'unquote-splicing',
// Macro-related
'define-syntax', 'let-syntax', 'letrec-syntax',
'define-syntax', 'let-syntax', 'letrec-syntax', 'define-macro',
// Control flow
'call/cc', 'call-with-current-continuation',
// Module system
Expand Down
8 changes: 8 additions & 0 deletions src/extras/scheme/define-macro.sld
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
;; Library for define-macro support
;; This library exports the define-macro special form.
;; It is loaded by default in the REPL environment.

(define-library (scheme-js define-macro)
(export define-macro)
(import (scheme base))
)
1 change: 1 addition & 0 deletions src/packaging/bundled_libraries.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading