Another little something on REPL. It can now complete symbols like
c-w-c-c. This is an old tradition of
We added a new module
to support this.
Hints for unbound variable error
While working on REPL, sometimes you accidentally try to evaluate a variable that isn't
visible from your current module.
It is a bit annoying if you know the module is loaded, just you forget
use it in the current module.
So we added a little feature. When REPL reports an unbound varriable error, it also lists if there are variables of that name, exported from modules that are loaded into the process but not visible from the evaluating module:
gosh$ (thread-start! (make-thread (^ (print "Hi")))) *** UNBOUND-VARIABLE-ERROR: unbound variable: make-thread NOTE: `make-thread' is exported from the following module: - gauche.threads Stack Trace: _______________________________________ 0 (report-error e) 1 (make-thread (^ () (print "Hi"))) at "(input string port)":1
It may be nice to show modules that aren't even loaded, too, but that would be too costly so we avoided it. It also doesn't show non-exported variables, which is debatable--sometimes you forgot to export one and that caused this error. Let's use this for a while and see if we need non-exported ones, too.
This is realized in a general mechanism in error reporting. We haven't documented it yet, for we may tweak the interface, but I'll show it to give the general idea.
The error message in REPL, including the stack trace, is produced by
report-error (ref:report-error). It prints
*** ... line,
with the condition class name and error message, then calls
a generic function
report-additional-condition on the condition.
We have a specialized method for
searches the name in the loaded modules and prints the hint.
If the thrown condition is a compound condition,
is called over each component of the compound condition. This allows
custom report for each component. When you load a file that has a statically
detectable error, you get the additional information (
While compiling ...).
It is also realized by the same mechanism. The compiler and the loader
adds the location information as a compound condition, and
report-additional-condition on them, which shows those additional
gosh> ,l ./foo *** ERROR: wrong number of arguments: cons requires 2, but got 1 While compiling "./foo.scm" at line 1: (define (bar x) (cons x)) While loading "./foo.scm" at line 2 Stack Trace: _______________________________________ 0 (report-error e) 1 (errorf "wrong number of arguments: ~a requires ~a, but got ~"... 2 (pass1/expand-inliner program id gval cenv)
:immutable slot option
Immutability is far more valued nowadays than when CLOS was designed. Back then, basically the system allows programmers to change almost everything, assuming he knows what he was doing.
However, specifying something immutable is not only to protect users from shooting their own foot; it communicates to the readers. Here the readers can be a programmer who reads and uses the code years later (who can be the same programmer who wrote it bot has forgotten the details), or a program-processing programs such as optimizer to take advantage of immutablity.
CLOS (and Gauche's object system) have enough flexibility to implement immutable slots, but it is somewhat awkward. It's not as simple as having a custom setter that throws an error; for, the slot setter is also called in the instance initializer which runs at the instance construction. You have to distinguish whether the slot setter is invoked during initialization or outside initialization, but such dynamic introspection would be costly.
We came up an alternative mechanism which is effectively realizes immutable slots in practical situations, but does not require to distinguish whether it's in initialization or not.
If a slot has a true value for
:immutable slot option, the slot can only
be initialized once--that is, the setter sets the value if the slot is previously
unbound, but throws an error if not. If you give the slot an initial value,
:init-value etc., then that one chance
to set the value is used within initializer. Uninitialized immutable slots
don't make much sense, so we expect almost always immutable slots are initialized
It is possible that the initializer leaves the slot unbound, and later
the user call
slot-set! to set it once. It can be viewed as delayed
(We first named this option
init-once, for the slot can be set once,
but changed our mind for it could be confusing.)
Source info propagation with macro expansion
Gauche tracks source code location information and shows it in the stack trace. However, what if the source is generated by macros? In 0.9.12, the macro expander re-attached the original source info to the outermost form of the macro output. However, if a runtime error occurred in constructed code other than the outermost one, stack trace couldn't find the info and had to show "[unknown location]". It was annoying especially when the code was the result of nested macro expansions, that you didn't get a clue about where the error came from.
I was annoyed enough, so from 0.9.13, you can have better stack trace. (Well, if you're familiar with other Scheme that employs syntax-case macro expander, you're already familiar with such a feature. Yes, Gauche finally caught up.)
Let's show it with a somewhat contrived example. The following
macro expands to
cxxx...xxr according to the given sequence
;; (cxr a r obj) == (car obj) ;; (cxr a a r obj) == (caar obj) ;; (cxr a d a r obj) == (cadar obj) ;;etc. (define-syntax cxr (syntax-rules (a d r) [(_ r obj) obj] [(_ a xs ...) (car (cxr xs ...))] [(_ d xs ...) (cdr (cxr xs ...))] [(_ . xs) (syntax-error "Malformed cxr:" (cxr . xs))]))
In 0.9.12, if you pass something that causes a runtime error, you get the annoying unknown location:
gosh$ (cxr a a a a r '(1 2 3 4)) *** ERROR: pair required, but got 1 Stack Trace: _______________________________________ 0 (car (cxr a r '(1 2 3 4))) [unknown location] 1 (eval expr env) at "/usr/share/gauche-0.98/0.9.12/lib/gauche/interactive.scm":336
In 0.9.13, you'll get this:
gosh$ (cxr a a a a r '(1 2 3 4)) *** ERROR: pair required, but got 1 Stack Trace: _______________________________________ 0 (car (cxr a r '(1 2 3 4))) at "/home/shiro/src/Gauche/test/macro-source-info.scm":15 expanded from (cxr a a r '(1 2 3 4)) at "/home/shiro/src/Gauche/test/macro-source-info.scm":15 expanded from (cxr a a a r '(1 2 3 4)) at "/home/shiro/src/Gauche/test/macro-source-info.scm":15 expanded from (cxr a a a a r '(1 2 3 4)) at "(standard input)":34 1 (eval expr env) at "/home/shiro/src/Gauche/src/../lib/gauche/interactive.scm":354
This works with ER-macro, too. Suppose we have another macro,
c*r, in which you can give
d's in a single symbol.
(c*r aada obj) is
We also let the code print the given symbol, just for the sake of
making things complicated.
;; (c*r aa obj) == print 'aa' and return (caar obj) ;; (c*r addar obj) == print 'addar' and return (caadr obj) ;; etc. (define-syntax c*r (er-macro-transformer (^[form rename cmp] (match form [(_ xs obj) (let1 cs (map ($ string->symbol $ string $) (string->list (symbol->string xs))) (quasirename rename `(begin (print ',xs) (cxr ,@cs r ,obj))))]))))
gosh$ (c*r aad '(1 2 3 4)) aad *** ERROR: pair required, but got 2 Stack Trace: _______________________________________ 0 (car (cxr a d r '(1 2 3 4))) [unknown location] 1 (eval expr env) at "/usr/share/gauche-0.98/0.9.12/lib/gauche/interactive.scm":336
gosh$ (c*r aad '(1 2 3 4)) aad *** ERROR: pair required, but got 2 Stack Trace: _______________________________________ 0 (car (cxr a d r '(1 2 3 4))) at "/home/shiro/src/Gauche/test/macro-source-info.scm":15 expanded from (cxr a a d r '(1 2 3 4)) at "/home/shiro/src/Gauche/test/macro-source-info.scm":60 expanded from (quasirename rename `(begin (print ',xs) (cxr ,@cs r ... at "/home/shiro/src/Gauche/test/macro-source-info.scm":57 1 (eval expr env) at "/home/shiro/src/Gauche/src/../lib/gauche/interactive.scm":354
Now, if you're user of
no wonder how it can be done. Macro output is constructed
as syntactic objects, which can carry any sideband information.
But with ER-macro, you construct the output as a simple S-expression, so
it's not obviouhs where those information comes from.
Gauche has an extended pair that can carry extra information other
than car and cdr. Those sideband data isn't visible as far as
you're treating it as a pair, nor it affects
the pairs. Source code information is stored there by
procedure and its families.
gosh$ (read-from-string "(a b c d)") (a b c d) gosh$ (pair-attributes *1) ((source-info "(input string port)" 1))
If you consturct lists with
list, those information
won't be attached. However,
quasirename does the trick.
It extracts the original source info from the input, and re-attaches
it to the constructed form.
Note that, for a macro expander, we need to consider
two kinds of source information: One is of the macro definition,
and another is of the macro input. The source info of the
macro definition is available through the argument of
But how can it get the macro input information? The macro input is
already deconstructed by the time
quasirename is called.
We use another sideband mechanism, procedure tags. Srfi-229 defines
a general mechanism to attach an arbitrary Scheme object to a procedure.
Gauche has more general mechanism (although not documented yet) that
a procedure can have multiple tags, and the macro input is attached
to the rename procedure as one of such tags. Then
extracts that information from the rename procedure and applies
it to the output.
In the definition of
quasirename, the output construction code
looks like this:
;; in src/libmacro.scm (if-let1 si (pair-attribute-get objs 'source-info #f) (let1 orig (assoc-ref ((with-module gauche.internal %procedure-tags-alist) r) 'macro-input) `(,extended-cons. ,xx ,yys '((source-info ,@si) ,@(cond-list [orig `(original . ,orig)])))) `(,cons. ,xx ,yys)))))
The pair attribute
source-info holds the source info of
macro definition, and
original holds the macro input form.
The disadvantage of having source info in the sideband data of pairs is, obviously, that you can't attach source info to other objects than pairs. I find it not a big issue in practice, for most expressions that need attention are function calls, macro calls or special forms.
On the other hand, it has an advantage that quoted literal lists can have
source code information. It can't be done with syntax objects,
quote strips any syntax wrappings. It is handy when you
put a literal nested structure as DSL and let its walker signals
an error with the location of the literal structure.
This is a desired feature and I'm happy to have it. However, I'm feeling a bit of ambivalence, too.
The reason I prefer ER-macro to syntax-case is that ER-macro is explicit---input and output are raw S-expression which you can direclty touch and rearrange. With syntax-case, things are wrapped in opaque syntax object, and even though you can unwrap and rewrap the objects, that opaqueness bothers me.
However, with this
quasirename modification, I did introduce
an implicit operation; even though the output of
can be treated as an ordinary S-expression, it does more to it than just
If I feel comfortable with this, maybe I can also feel comfortable
syntax-case, too. I don't know yet. Let's see.
Using Gauche in GitHub Actions
I created a GitHub action to install Gauche in the runner, so that you can use Gauche in subsequent steps: setup-gauche. Currently the action works on Linux and OSX.
To use the action, simply say
uses: shirok/setup-gauche@v3 in your job steps (check the latest version number in the setup-gauche page). The following is an excerpt of
.github/workflow/main.yml of Gauche-mecab:
jobs: build-and-test-linux: runs-on: ubuntu-latest timeout-minutes: 10 steps: - uses: actions/checkout@v3 - uses: shirok/setup-gauche@v3 - name: Install dependencies run: | sudo apt install -y libmecab-dev mecab-ipadic-utf8 - name: Build and check run: | ./configure make make -s check make -s check-dep
Gauche is installed in standard path (
/usr on Linux,
/usr/local on OSX) so that you can build Gauche extensions or run Gauche applications without any extra settings.
By default, it installs the latest release. You can choose a specific version of Gauche to install via
gauche-version input parameter; specifically, saying 'snapshot' installs the latest snapshot (prerelease) build, if there's any newer than the latest release.