View Javadoc

1   ////////////////////////////////////////////////////////////////////////////////
2   // MillScript: an Open Spice interpreter and batch website creation tool
3   // Copyright (C) 2004-2005 Kevin Rogers
4   //
5   // This file is part of MillScript.
6   //
7   // MillScript is free software; you can redistribute it and/or modify it under
8   // the terms of the GNU General Public License as published by the Free
9   // Software Foundation; either version 2 of the License, or (at your option)
10  // any later version.
11  //
12  // MillScript is distributed in the hope that it will be useful, but WITHOUT
13  // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
14  // FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
15  // more details.
16  //
17  // You should have received a copy of the GNU General Public License along with
18  // MillScript; if not, write to the Free Software Foundation, Inc., 59 Temple
19  // Place, Suite 330, Boston, MA  02111-1307  USA
20  ////////////////////////////////////////////////////////////////////////////////
21  package org.millscript.millscript.vm;
22  
23  import org.millscript.commons.alert.Alert;
24  import org.millscript.commons.alert.EscapeException;
25  import org.millscript.commons.util.EMap;
26  import org.millscript.commons.util.MapIterator;
27  import org.millscript.commons.util.map.EHashMap;
28  import org.millscript.commons.vfs.VEntry;
29  import org.millscript.commons.vfs.VFile;
30  import org.millscript.commons.vfs.VFolder;
31  import org.millscript.millscript.Source;
32  import org.millscript.millscript.alert.Alerts;
33  import org.millscript.millscript.alert.Phases;
34  import org.millscript.millscript.conf.Configuration;
35  import org.millscript.millscript.expr.GlobalIdent;
36  import org.millscript.millscript.functions.Function;
37  import org.millscript.millscript.loaders.Loader;
38  import org.millscript.millscript.syntax.ConfigurationParserImpl;
39  import org.millscript.millscript.syntax.Parser;
40  import org.millscript.millscript.syntax.ParserImpl;
41  import org.millscript.millscript.syntax.TokenType;
42  
43  import java.io.IOException;
44  import java.io.Reader;
45  import java.io.StringReader;
46  import java.util.HashSet;
47  import java.util.Iterator;
48  import java.util.List;
49  
50  /**
51   * This class represents a MillScript Package.
52   */
53  public class Package {
54  
55      /**
56       * Maps from symbols to Loaders. This is used to autoload values for
57       * symbols.
58       */
59      private final EMap< String, Loader > autoloadableSymbols = new EHashMap< String, Loader >();
60  
61      /**
62       * This packages configuration.
63       */
64      private final Configuration config;
65  
66      /**
67       * The engine this package will use to execute code
68       */
69      private final Engine engine;
70  
71      /**
72       * This packages fully qualified name.
73       */
74      private final String fullyQualifiedName;
75  
76      /**
77       * Maps from symbols to ident records.
78       */
79      private final EMap< String, GlobalIdent > globalSymbols = new EHashMap< String, GlobalIdent >();
80  
81      /**
82       * This packages imports, mapping a packages nickname to its package
83       * object.
84       */
85      private final EMap< String, Package > imports = new EHashMap< String, Package >();
86  
87      /**
88       * Indicates if this package is pervasive.
89       */
90      private final boolean isPervasive;
91  
92      /**
93       * Constructs a new package with the given fully qualified name and
94       * nickname.
95       *
96       * @param e     the engine this package is associated with
97       * @param fqn   the new packages fully qualified name
98       * @param b     a boolean indicating if this package is pervasive
99       */
100     public Package( final Engine e, final String fqn, final boolean b ) {
101         this.engine = e;
102         this.config = e.getConfig();
103         this.fullyQualifiedName = fqn;
104         this.isPervasive = b;
105     }
106 
107     /**
108      * Adds the contents of the specified Inventory to the set of autoloadable
109      * variables.
110      *
111      * @param dir the inventory to add
112      */
113     public void addInventory( final VFolder dir ) {
114         if ( dir != null && dir.exists() ) {
115             final List entries = dir.listEntries();
116             final Iterator it = entries.iterator();
117             while ( it.hasNext() ) {
118                 Loader loader = this.makeLoaderFor( (VEntry) it.next() );
119                 if ( loader != null ) {
120                     autoloadableSymbols.insert( loader.getSymbol(), loader );
121                 }
122             }
123         }
124     }
125 
126     /**
127      * Autoloads the bindings for the specified symbol.
128      *
129      * @param sym   the symbol to load bindings for
130      */
131     public void autoload( final String sym ) {
132         final Loader loader = autoloadableSymbols.get( sym );
133         if ( loader != null ) {
134             config.getLogger().info( "AUTOLOADING " );
135             config.getLogger().infoLine( sym );
136             try {
137                 loader.loadBindings();
138             } catch ( IOException ioex ) {
139                 throw(
140                     Alerts.compile( "IOException thrown while loading", null ).
141                     origin( loader ).
142                     culprit( "reason", ioex.getMessage() ).
143                     mishap()
144                 );
145             } catch ( Alert ex ) {
146                 // Get the originating alert, try to decorate it then remishap it
147                 throw(
148                     ex.origin( loader ).setPhase( Phases.AUTOLOAD ).remishap()
149                 );
150             }
151         }
152     }
153 
154     /**
155      * Returns an iterator over all the autoloadable symbols in this package.
156      *
157      * @return  an iterator over all the autoloadable symbols in this package.
158      */
159     public MapIterator< String, Loader > autoloadablesIterator() {
160         return autoloadableSymbols.iterator( true );
161     }
162 
163     /**
164      * Makes a binding in this package from the specified symbol to the
165      * specified object.
166      *
167      * @param sym   the symbol to bind the object to.
168      * @param obj   the object to bind to the specified symbol.
169      * @return  the GlobalIdent for the new global variable
170      */
171     public GlobalIdent bind( final String sym, final Object obj ) {
172         // Specifically check for null to stop Java going into an infinite loop
173         // and calling the wrong version of the bind method. Now that bind has
174         // been changed to bindRef, this is not as much of a problem.
175         Ref ref = null;
176         if ( obj == null ) {
177             ref = new Ref( obj );
178         } else if ( obj instanceof Function ) {
179             Function fn = (Function)obj;
180             if ( fn.getName() == null ) {
181                 fn.modName( sym );
182             }
183             ref = new Ref( fn );
184         } else if ( obj instanceof Ref ) {
185             ref = (Ref)obj;
186         } else {
187             ref = new Ref( obj );
188         }
189         return this.bindRef( sym, ref );
190     }
191 
192     /**
193      * Makes a binding in this package from the specified symbol to the
194      * specified object. The bound symbol is a const.
195      *
196      * @param sym   the symbol to bind the object to.
197      * @param obj   the object to bind to the specified symbol.
198      */
199     public void bindConst( final String sym, final Object obj ) {
200         final GlobalIdent gid = this.bind( sym, obj );
201         gid.setIsProtected( false );
202         gid.setIsConst( true );
203     }
204 
205     /**
206      * Makes a binding in this package from the specified symbol to the
207      * specified object. The bound symbol is a variable.
208      *
209      * @param sym   the symbol to bind the object to.
210      * @param obj   the object to bind to the specified symbol.
211      */
212     public void bindVar( final String sym, final Object obj ) {
213         final GlobalIdent gid = this.bind( sym, obj);
214         gid.setIsProtected( false );
215         gid.setIsConst( false );
216     }
217 
218     /**
219      * Makes a binding in this package from the specified symbol to the
220      * specified object. The bound symbol is a const.
221      *
222      * @param sym   the symbol to bind the object to.
223      * @param obj   the object to bind to the specified symbol.
224      */
225     public void bindProtectedConst( final String sym, final Object obj ) {
226         final GlobalIdent gid = this.bind( sym, obj );
227         gid.setIsProtected( true );
228         gid.setIsConst( true );
229     }
230 
231     /**
232      * Makes a binding in this package from the specified symbol to the
233      * specified object. The bound symbol is a variable.
234      *
235      * @param sym   the symbol to bind the object to.
236      * @param obj   the object to bind to the specified symbol.
237      */
238     public void bindProtectedVar( final String sym, final Object obj ) {
239         final GlobalIdent gid = this.bind( sym, obj );
240         gid.setIsProtected( true );
241         gid.setIsConst( false );
242     }
243 
244     /**
245      * Makes a binding in this package from the specified symbol to the
246      * specifed reference.
247      *
248      * @param sym   the symbol to bind the reference to.
249      * @param ref   the reference to bind to the specified symbol.
250      * @return  the GlobalIdent for the new global variable
251      */
252     protected GlobalIdent bindRef( final String sym, final Ref ref ) {
253         final String symbol = sym.intern();
254         final GlobalIdent gid = this.declareGlobal( symbol );
255         gid.getRef().value = ref.value;
256         return gid;
257     }
258 
259     /**
260      * Returns an iterator over all the bindings in this package.
261      *
262      * @return  an iterator over all the bindings in this package.
263      */
264     public MapIterator< String, GlobalIdent > bindingsIterator() {
265         return globalSymbols.iterator( true );
266     }
267 
268     /**
269      * Declares the specified symbol as a global symbol. The symbol will be
270      * bound to a <code>null</code> reference. This method is
271      *
272      * @param sym   the symbol to declare as a global variable
273      * @return  the GlobalIdent for the new global variable
274      */
275     public GlobalIdent declareGlobal( final String sym ) {
276         // TODO - Should this also check the autoloadables?
277         GlobalIdent id = globalSymbols.get( sym );
278         if ( id == null ) {
279             final Package origin = this.hasPervasiveImport( sym );
280             if ( origin != null ) {
281                 throw(
282                     Alerts.compile(
283                         "This variable is imported and cannot be shadowed",
284                         "It is imported from a pervasive package (e.g. the standard package)"
285                     ).
286                     culprit( "variable", sym ).
287                     culprit( "origin", origin.fullyQualifiedName ).
288                     mishap()
289                 );
290             } else {
291                 GlobalIdent gid = new GlobalIdent( sym, new Ref() );
292                 globalSymbols.insert( sym, gid );
293                 // TODO - We should probably check if this package is pervasive
294                 // and set setIsProtected and setIsConst to true, like bindRef
295                 return gid;
296             }
297         } else {
298             this.config.reportAlertAsWarning(
299                 Alerts.compile(
300                     "This variable is already declared",
301                     null
302                 ).culprit( "variable", sym )
303             );
304             return id;
305         }
306     }
307 
308     /**
309      * Returns the ident for the specified global symbol. The package which the
310      * symbol was imported with can be specified by the optional nickname.
311      *
312      * @param nn    the nickname of the package the request symbol was imported
313      * with
314      * @param sym   the symbol you wish to find.
315      * @return  the Ident for the specified symbol.
316      */
317     public GlobalIdent findIdentFor( final String nn, final String sym ) {
318         // TODO - Revisit this to use MillScript-Util
319         HashSet< String > foundIdents = new HashSet< String >();
320         if ( nn == null ) {
321             GlobalIdent id = this.loadIdent( sym );
322             if ( id == null ) {
323                 // This code used to specifically check if the symbol was part
324                 // of the standard package. I've removed this as the standard
325                 // package is pervasive nothing else can declare anything it
326                 // does.
327                 MapIterator< String, Package > it = imports.iterator( true );
328                 while ( it.hasNext() ) {
329                     String nick = it.nextKey();
330                     Package p = it.currentValue();
331                     GlobalIdent gid = p.loadIdent( sym );
332                     if ( gid != null ) {
333                         foundIdents.add( nick );
334                         id = gid;
335                     }
336                 }
337                 if ( foundIdents.size() > 1 ) {
338                     // We've found multiple definitions for this variable, thus
339                     // we have a problem.
340                     Alert alert = Alerts.compile(
341                         "This global name is ambiguous",
342                         "The following imported packages publish that name"
343                     ).culprit( "name", sym );
344                     // Attach information about where each definition occurs.
345                     Iterator< String > pkgIt = foundIdents.iterator();
346                     int pkgNum = 0;
347                     while ( pkgIt.hasNext() ) {
348                         String me = pkgIt.next();
349                         alert = alert.culprit( "package " + Integer.toString( pkgNum ) , me );
350                     }
351                     // Alert...
352                     throw alert.mishap();
353                 }
354             }
355             // Check if we found a definition for this symbol.
356             if ( id == null ) {
357                 throw(
358                     Alerts.compile(
359                         "Could not find the definition for this variable",
360                         null
361                     ).culprit( "name", sym ).mishap()
362                 );
363             }
364             // We only found one definition, so return it.
365             return id;
366         } else {
367             // Get the requested package.
368             Package p = imports.get( nn );
369             // Check if the requested package has been imported.
370             if ( p == null ) {
371                 throw(
372                     Alerts.compile(
373                         "Cannot access this package",
374                         "Perhaps this package has not been imported?"
375                     ).culprit( "nickname", nn ).mishap()
376                 );
377             }
378             // Try to find the symbol in the package
379             GlobalIdent id = p.loadIdent( sym );
380             // Did we find anything?
381             if ( id == null ) {
382                 throw(
383                     Alerts.compile(
384                         "Could not find the definition for this variable",
385                         null
386                     ).culprit( "nickname", nn ).culprit( "name", sym ).mishap()
387                 );
388             }
389             // Return the ident for the symbol
390             return id;
391         }
392     }
393 
394     /**
395      * Returns a new compiler state for this package.
396      *
397      * @return  a new compiler state for this package.
398      */
399     public CompilerState getCompilerState() {
400         final CompilerState cs = new CompilerState( engine );
401         cs.changePackage( this );
402         return cs;
403     }
404 
405     /**
406      * Returns this packages configuration.
407      *
408      * @return  this packages configuration
409      */
410     public Configuration getConfig() {
411         return config;
412     }
413 
414     /**
415      * Returns this packages fully qualified name.
416      *
417      * @return  this packages fully qualified name
418      */
419     public String getFullyQualifiedPackageName() {
420         return fullyQualifiedName;
421     }
422 
423     /**
424      * Returns <code>true</code> if the specified symbol is protected.
425      *
426      * @param sym   the symbol to test protected status for
427      * @return  <code>true</code> if the symbol is protected.
428      */
429     public boolean getIsProtected( final String sym ) {
430         // TODO - Should we also search all public imports.
431         // TODO - Should this also check the autoloadables?
432         // TODO - Should we use findIndentFor and put all this code in
433         // CompilerState
434         GlobalIdent id = globalSymbols.get( sym );
435         return id != null && id.getIsProtected();
436     }
437 
438     /**
439      * Returns true if the supplied symbol has a binding in this package.
440      *
441      * @param sym   the symbol to test for
442      * @return  <code>true</code> if the symbol has a binding and
443      * <code>false</code> otherwise.
444      * @todo    Should we also search all public imports.
445      */
446     public boolean hasBindingFor( final String sym ) {
447         // TODO - Should we also search all public imports.
448         // TODO - Should this also check the autoloadables?
449         return globalSymbols.get( sym ) != null;
450     }
451 
452     /**
453      * Looks to see if the specified symbol is already imported in a pervasive
454      * package.
455      *
456      * @param sym   the symbol to look for.
457      * @return  the pervasive Package defining the symbol or <code>null</code>
458      */
459     public final Package hasPervasiveImport( final String sym ) {
460         final MapIterator< String, Package > it = this.imports.iterator( true );
461         while ( it.hasNext() ) {
462             Package p = it.nextValue();
463             if ( p.isPervasive ) {
464                 // TODO - hasBindingFor should probably check the autoloadable
465                 // symbols and the following check would get more simple.
466                 if (
467                     p.hasBindingFor( sym ) ||
468                     p.autoloadableSymbols.get( sym ) != null
469                 ) {
470                     return p;
471                 }
472             }
473         }
474         return null;
475     }
476 
477     /**
478      * Imports the specified fully qualified package into this package, with
479      * the given nickname.
480      *
481      * @param nn    the nickname for the imported package
482      * @param fqn   the fully qualified name of the package to import.
483      */
484     public void importPackage( final String nn, final String fqn ) {
485         this.importPackage( nn, engine.getPackage( fqn ) );
486     }
487 
488     /**
489      * Imports the specified package object into this package, with the given
490      * nickname.
491      *
492      * @param nn    the nickname for the imported package
493      * @param p the package object to import.
494      */
495     public void importPackage( final String nn, final Package p ) {
496         imports.insert( nn, p );
497     }
498 
499     /**
500      * Interprets the bytes returned from reading the specified file as UTF-8
501      * text, into this package.
502      *
503      * @param f the File to read from.
504      */
505     public void interpret( final VFile f ) {
506         this.interpret( f.toString(), f.getReader() );
507     }
508 
509     /**
510      * Interprets the characters returned by the specified reader into this
511      * package. This may result in new variables being declared in this, or
512      * other packages.
513      *
514      * @param origin    the origin message for the reader
515      * @param r the reader to interpret characters from.
516      */
517     public void interpret( final String origin, final Reader r ) {
518         // This method is always considered non-interactive.
519         Parser parser = new ParserImpl( origin, r, true, config );
520         // This shoule compile everything that can be read, as the compile loop
521         // continues until the end of file.
522         this.getCompilerState().compileNoResults( parser );
523     }
524 
525     /**
526      * Interprets the contents of the specified string into this package. This
527      * may result in new variables being declared in this, or other packages.
528      *
529      * @param origin    the origin message for the reader
530      * @param s the string to interpret.
531      */
532     public void interpret( final String origin, final String s ) {
533         this.interpret( origin, new StringReader( s ) );
534     }
535 
536     /**
537      * Loads the specified configuration for this package.
538      *
539      * @param origin	the origin message for the configuration
540      * @param r	a reader to read the configuration from
541      */
542     public void loadConf( final String origin, final Reader r ) {
543         // Make a new configuration parser
544         final Parser confParser = new ConfigurationParserImpl(
545             origin,
546             r,
547             true,
548             this.config
549         );
550         // Get a new compiler state for compiling the configuration file
551         final CompilerState state = new CompilerState( this.engine );
552         // Change to the configuration package. Note we do not insert this
553         // package into the engine as an available import. It is used as
554         // the current package for the duration of configuration file
555         // compilation
556         state.changePackage( new ConfigurationPackage( this.engine, this, "millscript.configuration" ) );
557         try {
558             while ( confParser.peekToken() != TokenType.EOF ) {
559                 state.compileExpr( confParser.readExpr() );
560                 if ( this.engine.getMachine().getCount() > 0 ) {
561                     throw(
562                         Alerts.eval(
563                             "Package configuration trying to return results",
564                             "Results can only be returned at top level"
565                         ).origin( confParser ).mishap()
566                     );
567                 }
568             }
569         } catch ( Alert ex ) {
570             // Escape from here. The alert will be reported at the top
571             // level(hopefully). Absolutely, finally, try to decorate the
572             // alert, if it isn't already. This is required to catch problems
573             // with the parser.peekToken() line above.
574             // Oh, and report it too!
575             throw ex.origin( confParser ).escape();
576         } catch ( EscapeException ex ) {
577             // Just throw it again to escape outta' here
578             throw ex;
579         } catch ( Exception ex ) {
580             // Oh dear. We better give some indication of what happened...
581             throw(
582                 Alerts.parse(
583                     "Cannot parse configuration file",
584                     null
585                 ).culprit( "file", origin ).culprit( "reason", ex.getMessage() ).setParentThrowable( ex ).mishap()
586             );
587         }
588     }
589 
590     /**
591      * Loads the specified configuration for this package.
592      *
593      * @param packageConf   a File specifying the configuration file for this
594      * package.
595      */
596     public void loadConf( final VFile packageConf ) {
597         // Check if the file exists
598         if ( packageConf.exists() ) {
599             this.loadConf( packageConf.toString(), packageConf.getReader() );
600         }
601     }
602 
603     /**
604      * Loads the specified configuration for this package.
605      *
606      * @param source	a Source to read configuration from
607      */
608     public void loadConf( final Source source ) {
609         this.loadConf( source.getOrigin(), source.getReader() );
610     }
611 
612     /**
613      * Returns the global ident for the specified symbol, attempting to
614      * autoload it if available.
615      *
616      * @param sym   the symbol to get the ident for
617      * @return  the Globaldent for the specified symbol
618      */
619     private GlobalIdent loadIdent( final String sym ) {
620         GlobalIdent id = globalSymbols.get( sym );
621         if ( id != null ) {
622             return id;
623         }
624         this.autoload( sym );
625         return globalSymbols.get( sym );
626     }
627 
628     /**
629      * Returns the relevant loader for the specified virtual entry, if one is
630      * available.
631      *
632      * @param e the VEntry to make a loader for.
633      * @return  a loader for the specified file or <code>null</code>
634      */
635     public Loader makeLoaderFor( final VEntry e ) {
636         return this.config.getLoaderBuilder().getLoaderFor( this, e );
637     }
638 
639     /**
640      * Returns the reference associated with the specified symbol.
641      *
642      * @param sym   the symbol to find a value for.
643      * @return  the reference associated with the specified symbol.
644      * @todo    Should we also search all public imports.
645      */
646     public Object valueOf( final String sym ) {
647         // TODO - Should we also search all public imports.
648         // TODO - Shouldn't this also check the autoloadables?
649         final GlobalIdent id = globalSymbols.get( sym );
650         if ( id != null ) {
651             return id.getValue();
652         } else {
653             throw(
654                 Alerts.compile(
655                     "Could not find the definition for this variable",
656                     null
657                 ).culprit( "name", sym ).mishap()
658             );
659         }
660     }
661 
662 }