Proposal: with-permits

Overview

The idea of permits is to allow 2 contradictory things: "quick and dirty" code, and guarantees of clean code. This applies to behaviors like referential transparency, go to tags, unboxing values, "sandbox" issues, et al

Permits would let clean behavior be turned off for specific pieces of code.

Permits also allow you to guarantee clean code for entire call-trees. In other words, if you call function foo that denies certain permissions, you can be sure those permissions are also denied to any function called by foo, any function called by a function called by foo, etc.

What permits actually do

What permits actually do is specific to the permit in question.

For example, a referential-non-transparency permit, when turned off, mite disallow assigning to symbols' value slots except by `let' and `let*', and also instruct the compiler to optimize using the knowledge that every symbol was referentially transparent within the given form's call-tree.

In general, permits would require some degree of co-operation from the interpreter and the compiler.

Syntax

with-permits

      (with-permits PERMIT-SET &rest FORMS ) 
    

sets the permit set for FORMS to PERMIT-SET

PERMIT-SET is an unordered list of permits.

Single permits

A single permit has 3 fields.

      (PERMIT-KEY DIRECT-PERMISSIONS &optional INDIRECT-PERMISSIONS)
    

PERMIT-KEY

PERMIT-KEY above indicates which permit is being described. It must be a member of +all-permit-keys+

Both DIRECT-PERMISSIONS and INDIRECT-PERMISSIONS must be one of:

Defaults

If INDIRECT-PERMISSIONS is not provided, it defaults to the DIRECT-PERMISSIONS set of the respective key in +permits+. NB to deny permission to an entire call-tree, you need to set INDIRECT-PERMISSIONS explicitly to nil.

Keys that don't appear

Keys that don't appear in a permit set have whatever value they have in +permits+.

Keys appearing more than once

If a key appears more than once, the effect is as if its direct permission set were a union of the two direct permission sets, again treating `t' as the universal set. The indirect permit set is treated similarly.

Example:

      (with-permits 
	( 
	  (write-specials (*global-0* *global-1*) t)
	  (write-specials (*global-1* *global-2*) (*global-3*)))
	BODY)
    

behaves like

      (with-permits 
        ( 
          (write-specials (*global-0* *global-1* *global-2*) t))
        BODY)
    

Behavior

Passing control to other code

"Other code" is understood to mean both code contained in separate `with-permits' blocks and other lambda forms.

It is an error to call other code whose permits are more permissive than the current direct and indirect permits are. This applies to both direct and indirect permits.

More formally, it is an error if, for any permit key, the other code's objects for that key, both direct and indirect, are not each a not neccessarily proper subset of the union of the current objects for that permit key, where `t' is treated as the universal set of all objects.

Or in still other words, indirect permits may be promoted to direct permits, direct permits may be demoted, old permits may be destroyed, but new permits can't be created.

Example

      (defun foo ()
	""
	(with-permits 
	  ( (write-files nil) 
	    (write-specials nil t))

	  ;;Error because we ourselves can't write to special vars
	  (setq *my-global* t)

	  ;;OK because it only makes indirect permits direct.
	  (with-permits ((write-specials t))
	    ;;OK because this code can write to special vars
	    (setq *my-global* t))

	  (with-permits ((write-specials (*some-other-global*)))
	    ;;Error because that's not the global we gave permission
            ;;to write to. 
	    (setq *my-global* t))
  
	  ;;Error, because it expands the allowed behaviors.
	  (with-permits ((write-files t))
	    (do-some-stuff))))
    

Macros

Macros occupy a seperate call-tree of permittedness, because they are not neccessarily called as part of the run-time code. This allows them to act on code that otherwise would have different allowedness, to run in different compile-time environments, etc.

In turn, any macros that a macro itself uses (as opposed to generating code that uses it) belong to yet another call-tree, and so forth. Which is to say, permits do not interfere with compile-time / run-time distinctions.

Whole functions

If a function's body, not including docstring and any initial declare statements, is wholly surrounded by with-permits, it affects what may call the function. A call to

      (defun X Y (with-permits R . body )) 
    

is interpreted as:

   (with-permits R 
     (funcall (lambda Y . body) args... ))
    

All functions whose bodies aren't surrounded by with-permits behave as if R was replaced with +permits+.

I'd have liked `:permits' to be an extra param to defun to indicate that, because it isn't purely an elcosure issue. Something like:

      (defun foo ()
        :permits ((write-files t) (write-specials nil t) )
        :doc ""
        :body
        ...
      )
    

But since overall defun syntax is set in stone, I'll settle for a special rule for `with-permits' when it encloses the whole body.

Checking unexplored branches

Checking that unexplored branches have proper permits is only required of compilers. For interpreted code, a conforming implementation need only check the branches that are actually taken.

In other words, an interpreter need only pay attention to the final interpreted stream of operations, not to the overall structure of the code. But it will still have to distinguish between calls, eg to macros, that "really" happen at compile-time and normal calls.

Compile-time evaluation

permit-sets are evaluated at compile-time. EG, the following would not be legal:

      (defun wrong-1 (my-permits)
        ""
        (with-permits 
          ;;ERROR
          my-permits 
          (do-stuff)))
    

Constants

+permits+

The constant +permits+ is a list of permits. They are the maximal permits the implementation allows, and there is no way to exceed them.

Motivation: Implementations may want to disallow certain behavior that they don't support. This would quickly make it clear that certain code ported from other systems won't work and why, and could help programmers find the exact areas that need to be changed.

EG, a Lisp dialect that did not support arrays mite not permit any distinction between arrays and simple-vectors.

To be determined

Probably +permits+ will place all permit-objects in direct permissions, and none in indirect permissions. Should this be required? It makes a difference wrt default values for INDIRECT-PERMISSIONS.

+all-permit-keys+

The constant +all-permit-keys+ is a list of all known permit keys.

Late alterations to +permits+

An implementation may define means of altering +permits+ to address new types of permits that weren't foreseen when the system was built. This would let old systems remain in formal compliance with new standards simply by defining the new permit keys as nil.

Some suggested permits: