diff --git a/runtime/src/main/fusion/modules/fusion/private/procedure.fusion b/runtime/src/main/fusion/modules/fusion/private/procedure.fusion new file mode 100644 index 000000000..36ea926a3 --- /dev/null +++ b/runtime/src/main/fusion/modules/fusion/private/procedure.fusion @@ -0,0 +1,33 @@ +// Copyright Ion Fusion contributors. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +(module private_procedure "/fusion/private/builtins" + + (require + "/fusion/experimental/syntax" + "/fusion/private/bind" + "/fusion/private/sexp" + "/fusion/unsafe/sexp") + + (provide thunk_expander) + + // This needs to be in a separate mod + (define (thunk_expander stx) + ''' +Shared implementation of `||` and `thunk`. + +This needs to be in a separate module because `define_syntax` bodies are +compiled before `define`s are expanded, so they cannot reference helpers from +the same module. Racket has `define_for_syntax` to create helpers at the +correct expansion phase. + ''' + (let [(body (unsafe_pair_tail (syntax_unwrap stx)))] + (if (is_pair body) + (datum_to_syntax + (pair (quote_syntax lambda) + (pair (quote_syntax ()) + body)) + (quote_syntax context) + stx) + (wrong_syntax stx "Expected at least one body form")))) +) diff --git a/runtime/src/main/fusion/modules/fusion/procedure.fusion b/runtime/src/main/fusion/modules/fusion/procedure.fusion index 3244c8f8f..2c305119e 100644 --- a/runtime/src/main/fusion/modules/fusion/procedure.fusion +++ b/runtime/src/main/fusion/modules/fusion/procedure.fusion @@ -91,6 +91,7 @@ of bound identifiers: "/fusion/private/bind" "/fusion/private/compare" "/fusion/private/control" + "/fusion/private/procedure" "/fusion/private/sexp" "/fusion/unsafe/list" "/fusion/unsafe/sexp") @@ -163,20 +164,11 @@ always returns `v`. Returns a zero-argument procedure that evaluates the `body` forms. Equivalent to `(lambda () body ...)`. ''' - (lambda (stx) - (let [(body (unsafe_pair_tail (syntax_unwrap stx)))] - (if (is_pair body) - (datum_to_syntax - (pair (quote_syntax lambda) - (pair (quote_syntax ()) - body)) - (quote_syntax context) - stx) - (wrong_syntax stx "Expected at least one body form"))))) + thunk_expander) // TODO This should use (provide (rename_out (thunk ||))) // but the documentation is wrong. - (defpub || + (defpub_syntax || ''' (|| body ...+) @@ -184,7 +176,9 @@ Returns a zero-argument procedure that evaluates the `body` forms. Equivalent to `(lambda () body ...)`. See also [`|`](fusion/procedure.html#|). - ''' thunk) + ''' + thunk_expander) + (defpub (compose p1 p2) ''' diff --git a/runtime/src/main/java/dev/ionfusion/fusion/SyntaxSymbol.java b/runtime/src/main/java/dev/ionfusion/fusion/SyntaxSymbol.java index 981aafe50..b7dbfd691 100644 --- a/runtime/src/main/java/dev/ionfusion/fusion/SyntaxSymbol.java +++ b/runtime/src/main/java/dev/ionfusion/fusion/SyntaxSymbol.java @@ -305,6 +305,12 @@ SyntaxValue doExpand(Expander expander, Environment env) throw new SyntaxException(null, message, this); } + if (resolveSyntaxMaybe(env) != null) + { + String message = "Invalid use of syntax form as identifier expression."; + throw new SyntaxException(null, message, this); + } + Binding b = resolve(); if (b instanceof FreeBinding) { diff --git a/runtime/src/main/java/dev/ionfusion/fusion/_Private_HelpForm.java b/runtime/src/main/java/dev/ionfusion/fusion/_Private_HelpForm.java index efd2c1e84..58c7375fa 100644 --- a/runtime/src/main/java/dev/ionfusion/fusion/_Private_HelpForm.java +++ b/runtime/src/main/java/dev/ionfusion/fusion/_Private_HelpForm.java @@ -69,6 +69,7 @@ public void write(Evaluator eval, Appendable out) SyntaxValue expand(Expander expander, Environment env, SyntaxSexp stx) throws FusionException { + // TODO reject if not at top level final Evaluator eval = expander.getEvaluator(); SyntaxChecker check = check(eval, stx); @@ -87,7 +88,14 @@ SyntaxValue expand(Expander expander, Environment env, SyntaxSexp stx) for (int i = 1; i < arity; i++) { SyntaxSymbol identifier = check.requiredIdentifier(i); - children[i] = expander.expandExpression(env, identifier); + + // We don't want to expand the identifier since it might be syntax + // and that will fail. But we do want to determine its binding so + // we can look up documentation at runtime. This may resolve to a + // FreeBinding, which will trigger an unbound-identifier error + // during compilation, which is appropriate. + + identifier.resolve(); } return stx.copyReplacingChildren(eval, children); diff --git a/runtime/src/test/fusion/scripts/bool.test.fusion b/runtime/src/test/fusion/scripts/bool.test.fusion index d5398f5e1..ddf2c8915 100644 --- a/runtime/src/test/fusion/scripts/bool.test.fusion +++ b/runtime/src/test/fusion/scripts/bool.test.fusion @@ -7,9 +7,13 @@ // Here's the bindings we expect to be available: (module check_bindings '/fusion/bool' [ - and, cond, if, is_bool, + // These syntax forms cannot be verified this way. + // https://github.com/ion-fusion/fusion-java/issues/122 + // and, cond, if, or, unless, when + + is_bool, is_false, is_true, is_truthy, is_untruthy, - not, or, unless, when, + not, ]) diff --git a/runtime/src/test/fusion/scripts/syntax.test.fusion b/runtime/src/test/fusion/scripts/syntax.test.fusion index 6030cd450..1e9db0cb5 100644 --- a/runtime/src/test/fusion/scripts/syntax.test.fusion +++ b/runtime/src/test/fusion/scripts/syntax.test.fusion @@ -24,6 +24,10 @@ (datum_to_syntax [id] id) +// Syntax forms cannot be used as expressions +(expect_syntax_error (list 1 if 2)) + + //========================================================================== // is_identifier