Skip to content

Commit

Permalink
Add imap to world DSL, with examples (#1990)
Browse files Browse the repository at this point in the history
Adds a new primitive `imap`, aka index map, primitive to the world DSL.  `imap` has type `World int -> World int -> World a -> World a`.  Think of it like `(Coords -> Coords) -> World a -> World a`, i.e. given a coordinate mapping, it creates a new world by looking up the cell at the transformed coordinates in the given base world.  However, since there are no lambdas we cannot directly give it that type; instead, the first `World int` represents a function `Coords -> int` which gives an x coordinate, and the second gives the y coordinate.  All told, `imap wx wy wa` is like `\c -> wa (wx c, wy c)`.  For example, `imap (-x) y w` is a reflection of `w` across the y-axis.

Adds a description of `imap` to the language reference, as well as adding a few examples.

Also removes the `rot` and `reflect` primitives, since they can now be simply implemented in terms of `imap`.

Depends on merging #1989 first.  Closes #1584.
  • Loading branch information
byorgey authored Jun 26, 2024
1 parent 939ecf3 commit 1735416
Show file tree
Hide file tree
Showing 14 changed files with 178 additions and 50 deletions.
1 change: 1 addition & 0 deletions data/scenarios/00-ORDER.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ Speedruns
Testing
Vignettes
Mechanics
World Examples
1 change: 1 addition & 0 deletions data/scenarios/Testing/1320-world-DSL/00-ORDER.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ constant.yaml
erase.yaml
override.yaml
coords.yaml
reflect.yaml
46 changes: 46 additions & 0 deletions data/scenarios/Testing/1320-world-DSL/reflect.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
version: 1
name: Reflection (imap) test
description: |
A world with both horizontal and vertical reflection symmetry,
created with 'imap'.
creative: false
objectives:
- goal:
- Pick up four trees
condition: |
as base {n <- count "tree"; return (n >= 4)}
robots:
- name: base
loc: [0, 0]
dir: north
devices:
- logger
- grabber
- treads
- branch predictor
- scanner
- ADT calculator
- comparator
- GPS receiver
- bitcoin
solution: |
def x = \n. \c. if (n==0) {} {c; x (n-1) c} end
def ifC = \p. \t. \e. b <- p; if b t e end
def findTree = ifC (ishere "tree") {whereami} {move; findTree} end
def ell = \d. turn right; x (2*d) move; grab; return () end
def grabTrees = \loc. let x = fst loc in let y = snd loc in grab; ell y; ell x; ell y end
n <- random 10;
x (n+1) move; turn right; move;
loc <- findTree;
grabTrees loc
known: [tree]
world:
dsl: |
let trees = if (hash % 4 == 0) then {tree, dirt} else {stone}
in
overlay
[ mask (x >= 0 && y >= 0) trees
, mask (x >= 0 && y < 0) (imap x (-y) trees)
, mask (x < 0 && y >= 0) (imap (-x) y trees)
, mask (x < 0 && y < 0) (imap (-x) (-y) trees)
]
4 changes: 4 additions & 0 deletions data/scenarios/World Examples/00-ORDER.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
clearing.yaml
rorschach.yaml
stretch.yaml
translate.yaml
22 changes: 22 additions & 0 deletions data/scenarios/World Examples/clearing.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
version: 1
name: Clearing
description: |
The base is in a clearing in the forest: the area within a certain
radius of the base is completely clear of trees; then there are
random trees at increasing density up to another radius; outside of
the outer radius there are only trees.
creative: true
robots:
- name: base
display:
char: Ω
loc: [0, 0]
dir: north
world:
dsl: |
overlay
[ {dirt}
, mask ((x*x + 4*y*y) >= (6*6) && (x*x + 4*y*y) <= (30*30))
(let h = hash % 24 in if (36 + h*h) <= (x*x + 4*y*y) then {tree,dirt} else {dirt} )
, mask ((x*x + 4*y*y) > (30*30)) {tree, dirt}
]
21 changes: 21 additions & 0 deletions data/scenarios/World Examples/rorschach.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
version: 1
name: Rorschach
description: |
A world with both horizontal and vertical reflection symmetry,
created with `imap`{=snippet}.
creative: true
robots:
- name: base
dir: north
loc: [0, 0]
known: [tree]
world:
dsl: |
let trees = if (hash % 4 == 0) then {tree, dirt} else {stone}
in
overlay
[ mask (x >= 0 && y >= 0) trees
, mask (x >= 0 && y < 0) (imap x (-y) trees)
, mask (x < 0 && y >= 0) (imap (-x) y trees)
, mask (x < 0 && y < 0) (imap (-x) (-y) trees)
]
16 changes: 16 additions & 0 deletions data/scenarios/World Examples/stretch.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
version: 1
name: Stretch
description: |
A world created by stretching a random pattern of trees, with the
amount of stretching determined by the distance from the origin.
creative: true
robots:
- name: base
dir: north
loc: [0, 0]
known: [tree]
world:
dsl: |
let trees = if (hash % 4 == 0) then {tree, dirt} else {stone}
in
imap (if (y == 0) then 0 else (x/abs(y))) (if (abs x <= 1) then 0 else (y/abs(x/2))) trees
24 changes: 24 additions & 0 deletions data/scenarios/World Examples/translate.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
version: 1
name: Translate
description: |
An illustration of how to use `imap`{=snippet} to translate. A basic patch is
created and then overlaid at various translations. Note that since
`imap`{=snippet} works by mapping a function over the coordinates, translation
is "backwards": for example, `imap (x+4)`{=snippet} translates 4 units to the
*left*.
creative: true
robots:
- name: base
dir: north
loc: [0, 0]
known: [rock]
world:
dsl: |
let patch = mask (abs(x) <= 4 && abs(y) <= 4) (if ((x + y) % 2 == 0) then {rock, dirt} else {dirt})
in
overlay
[ patch
, imap (x+6) (y+3) patch
, imap (x-10) (y-7) patch
, imap (x-14) (y+5) patch
]
6 changes: 6 additions & 0 deletions data/worlds/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ repetitions of `S` separated by `,`.
| 'let' (<ident> '=' <exp>)*, 'in' <exp>
| 'overlay' '[' <exp>+, ']'
| 'mask' <atom> <atom>
| 'imap' <atom> <atom> <atom>
| '"' <nonquote>+ '"'
| '(' <exp> ')'
Expand Down Expand Up @@ -186,4 +187,9 @@ entities but also some empty cells.
https://libnoise.sourceforge.net/glossary/index.html#perlinnoise
- `mask b e` takes the value of `e` where `b` is true, and is empty
elsewhere.
- `imap` has type `World int -> World int -> World a -> World a`, and
creates a new world from a reference world using the given index
lookup functions. That is, `imap wx wy wa` yields the world
`\c -> wa (wx c, wy c)`. For example, `imap (-x) y w` reflects the
world `w` across the line `y = 0`.
- `"foo"` imports the DSL term in `worlds/foo.world`.
22 changes: 12 additions & 10 deletions src/swarm-scenario/Swarm/Game/World/Compile.hs
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,9 @@ import Data.Tagged (Tagged (unTagged))
import Numeric.Noise.Perlin (noiseValue, perlin)
import Swarm.Game.Location (pattern Location)
import Swarm.Game.World.Abstract (BTerm (..))
import Swarm.Game.World.Coords (Coords (..), coordsToLoc)
import Swarm.Game.World.Coords (Coords (..), coordsToLoc, locToCoords)
import Swarm.Game.World.Gen (Seed)
import Swarm.Game.World.Interpret (interpReflect, interpRot)
import Swarm.Game.World.Syntax (Axis (..), Rot, World)
import Swarm.Game.World.Syntax (Axis (..), World)
import Swarm.Game.World.Typecheck (Applicable (..), Const (..), Empty (..), NotFun, Over (..))
import Witch (from)
import Witch.Encoding qualified as Encoding
Expand Down Expand Up @@ -71,9 +70,8 @@ compileConst seed = \case
CCoord ax -> CFun $ \(CConst (coordsToLoc -> Location x y)) -> CConst (fromIntegral (case ax of X -> x; Y -> y))
CHash -> compileHash seed
CPerlin -> compilePerlin
CReflect ax -> compileReflect ax
CRot rot -> compileRot rot
COver -> binary (<!>)
CIMap -> compileIMap
K -> CFun $ \x -> CFun $ const x
S -> CFun $ \f -> CFun $ \g -> CFun $ \x -> f $$ x $$ (g $$ x)
I -> CFun id
Expand Down Expand Up @@ -110,11 +108,15 @@ compilePerlin =
where
sample (i, j) noise = noiseValue noise (fromIntegral i / 2, fromIntegral j / 2, 0)

compileReflect :: Axis -> CTerm (World a -> World a)
compileReflect ax = CFun $ \w -> CFun $ \(CConst c) -> w $$ CConst (interpReflect ax c)

compileRot :: Rot -> CTerm (World a -> World a)
compileRot rot = CFun $ \w -> CFun $ \(CConst c) -> w $$ CConst (interpRot rot c)
compileIMap :: NotFun a => CTerm (World Integer -> World Integer -> World a -> World a)
compileIMap =
CFun $ \wx ->
CFun $ \wy ->
CFun $ \wa ->
CFun $ \c ->
let mkCoords :: CTerm Integer -> CTerm Integer -> CTerm Coords
mkCoords (CConst x) (CConst y) = CConst (locToCoords (Location (fromIntegral x) (fromIntegral y)))
in wa $$ mkCoords (wx $$ c) (wy $$ c)

type family NoFunParams a :: Constraint where
NoFunParams (a -> b) = (NotFun a, NoFunParams b)
Expand Down
23 changes: 3 additions & 20 deletions src/swarm-scenario/Swarm/Game/World/Interpret.hs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@
module Swarm.Game.World.Interpret (
interpBTerm,
interpConst,
interpReflect,
interpRot,
) where

import Control.Applicative (Applicative (..))
Expand All @@ -20,9 +18,9 @@ import Data.Tagged (unTagged)
import Numeric.Noise.Perlin (noiseValue, perlin)
import Swarm.Game.Location (pattern Location)
import Swarm.Game.World.Abstract (BTerm (..))
import Swarm.Game.World.Coords (Coords (..), coordsToLoc)
import Swarm.Game.World.Coords (Coords (..), coordsToLoc, locToCoords)
import Swarm.Game.World.Gen (Seed)
import Swarm.Game.World.Syntax (Axis (..), Rot (..))
import Swarm.Game.World.Syntax (Axis (..))
import Swarm.Game.World.Typecheck (Const (..), Empty (..), Over (..))
import Witch (from)
import Witch.Encoding qualified as Encoding
Expand Down Expand Up @@ -64,27 +62,12 @@ interpConst seed = \case
let noise = perlin (fromIntegral s) (fromIntegral o) k p
sample (i, j) = noiseValue noise (fromIntegral i / 2, fromIntegral j / 2, 0)
in \(Coords ix) -> sample ix
CReflect ax -> \w -> w . interpReflect ax
CRot r -> \w -> w . interpRot r
CFI -> fromInteger
COver -> (<!>)
CIMap -> \wx wy a c -> a (locToCoords (Location (fromIntegral (wx c)) (fromIntegral (wy c))))
K -> const
S -> (<*>)
I -> id
B -> (.)
C -> flip
Φ -> liftA2

-- | Interprect a reflection.
interpReflect :: Axis -> Coords -> Coords
interpReflect ax (Coords (r, c)) = Coords (case ax of X -> (r, -c); Y -> (-r, c))

-- | Interpret a rotation.
interpRot :: Rot -> Coords -> Coords
interpRot rot (Coords crd) = Coords (rotTuple rot crd)
where
rotTuple = \case
Rot0 -> id
Rot90 -> \(r, c) -> (-c, r)
Rot180 -> \(r, c) -> (-r, -c)
Rot270 -> \(r, c) -> (c, -r)
10 changes: 10 additions & 0 deletions src/swarm-scenario/Swarm/Game/World/Parse.hs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ reservedWords =
, "mask"
, "empty"
, "abs"
, "imap"
]

-- | Skip spaces and comments.
Expand Down Expand Up @@ -139,6 +140,7 @@ parseWExpAtom =
<|> parseOverlay
<|> parseMask
<|> parseImport
<|> parseIMap
-- <|> parseCat
-- <|> parseStruct
<|> parens parseWExp
Expand Down Expand Up @@ -238,6 +240,14 @@ parseMask = do
parseImport :: Parser WExp
parseImport = WImport . into @Text <$> between (symbol "\"") (symbol "\"") (some (satisfy (/= '"')))

parseIMap :: Parser WExp
parseIMap = do
reserved "imap"
wx <- parseWExpAtom
wy <- parseWExpAtom
wa <- parseWExpAtom
return $ WOp IMap [wx, wy, wa]

-- parseCat :: Parser WExp
-- parseCat =
-- WCat
Expand Down
13 changes: 1 addition & 12 deletions src/swarm-scenario/Swarm/Game/World/Syntax.hs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ module Swarm.Game.World.Syntax (
RawCellVal,
CellTag (..),
CellVal (..),
Rot (..),
Var,
Axis (..),
Op (..),
Expand Down Expand Up @@ -65,16 +64,6 @@ instance PrettyPrec CellVal where
++ [(Just CellEntity, e ^. entityName) | EJust (Last e) <- [ent]]
++ map ((Just CellRobot,) . view trobotName) rs

data Rot = Rot0 | Rot90 | Rot180 | Rot270
deriving (Eq, Ord, Show, Bounded, Enum)

instance PrettyPrec Rot where
prettyPrec _ = \case
Rot0 -> "rot0"
Rot90 -> "rot90"
Rot180 -> "rot180"
Rot270 -> "rot270"

type Var = Text

data Axis = X | Y
Expand All @@ -83,7 +72,7 @@ data Axis = X | Y
instance PrettyPrec Axis where
prettyPrec _ = \case X -> "x"; Y -> "y"

data Op = Not | Neg | And | Or | Add | Sub | Mul | Div | Mod | Eq | Neq | Lt | Leq | Gt | Geq | If | Perlin | Reflect Axis | Rot Rot | Mask | Overlay | Abs
data Op = Not | Neg | And | Or | Add | Sub | Mul | Div | Mod | Eq | Neq | Lt | Leq | Gt | Geq | If | Perlin | Mask | Overlay | Abs | IMap
deriving (Eq, Ord, Show)

------------------------------------------------------------
Expand Down
19 changes: 11 additions & 8 deletions src/swarm-scenario/Swarm/Game/World/Typecheck.hs
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,8 @@ data Const :: Type -> Type where
CCoord :: Axis -> Const (World Integer)
CHash :: Const (World Integer)
CPerlin :: Const (Integer -> Integer -> Double -> Double -> World Double)
CReflect :: Axis -> Const (World a -> World a)
CRot :: Rot -> Const (World a -> World a)
COver :: (Over a, NotFun a) => Const (a -> a -> a)
CIMap :: NotFun a => Const (World Integer -> World Integer -> World a -> World a)
-- Combinators generated during elaboration + variable abstraction
K :: Const (a -> b -> a)
S :: Const ((a -> b -> c) -> (a -> b) -> a -> c)
Expand Down Expand Up @@ -186,9 +185,8 @@ instance PrettyPrec (Const α) where
CCoord ax -> ppr ax
CHash -> "hash"
CPerlin -> "perlin"
CReflect ax -> case ax of X -> "vreflect"; Y -> "hreflect"
CRot rot -> ppr rot
COver -> "over"
CIMap -> "imap"
K -> "K"
S -> "S"
I -> "I"
Expand Down Expand Up @@ -403,6 +401,13 @@ checkOver (TTyBase BFloat) a = a
checkOver (TTyBase BCell) a = a
checkOver ty _ = throwError $ NoInstance "Over" ty

checkNotFun :: (Has (Throw CheckErr) sig m) => TTy ty -> (NotFun ty => m a) -> m a
checkNotFun (TTyBase BBool) a = a
checkNotFun (TTyBase BInt) a = a
checkNotFun (TTyBase BFloat) a = a
checkNotFun (TTyBase BCell) a = a
checkNotFun ty _ = throwError $ NoInstance "NotFun" ty

------------------------------------------------------------
-- Existential wrappers

Expand Down Expand Up @@ -532,10 +537,9 @@ inferOp [SomeTy tyA] Gt = Some (tyA :->: tyA :->: TTyBool) <$> checkOrd tyA (ret
inferOp [SomeTy tyA] Geq = Some (tyA :->: tyA :->: TTyBool) <$> checkOrd tyA (return $ embed CGeq)
inferOp [SomeTy tyA] If = return $ Some (TTyBool :->: tyA :->: tyA :->: tyA) (embed CIf)
inferOp _ Perlin = return $ Some (TTyInt :->: TTyInt :->: TTyFloat :->: TTyFloat :->: TTyWorld TTyFloat) (embed CPerlin)
inferOp [SomeTy tyA] (Reflect r) = return $ Some (TTyWorld tyA :->: TTyWorld tyA) (embed (CReflect r))
inferOp [SomeTy tyA] (Rot r) = return $ Some (TTyWorld tyA :->: TTyWorld tyA) (embed (CRot r))
inferOp [SomeTy tyA] Mask = Some (TTyWorld TTyBool :->: TTyWorld tyA :->: TTyWorld tyA) <$> checkEmpty tyA (return $ embed CMask)
inferOp [SomeTy tyA] Overlay = Some (tyA :->: tyA :->: tyA) <$> checkOver tyA (return $ embed COver)
inferOp [SomeTy tyA] IMap = Some (TTyWorld TTyInt :->: TTyWorld TTyInt :->: TTyWorld tyA :->: TTyWorld tyA) <$> checkNotFun tyA (return $ embed CIMap)
inferOp tys op = error $ "bad call to inferOp: " ++ show tys ++ " " ++ show op

-- | Given a raw operator and the terms the operator is applied to,
Expand All @@ -553,10 +557,9 @@ inferOp tys op = error $ "bad call to inferOp: " ++ show tys ++ " " ++ show op
typeArgsFor :: Op -> [Some (TTerm g)] -> [SomeTy]
typeArgsFor op (t : _)
| op `elem` [Neg, Abs, Add, Sub, Mul, Div, Mod, Eq, Neq, Lt, Leq, Gt, Geq] = [getBaseType t]
typeArgsFor (Reflect _) (t : _) = [getBaseType t]
typeArgsFor (Rot _) (t : _) = [getBaseType t]
typeArgsFor op (_ : t : _)
| op `elem` [If, Mask, Overlay] = [getBaseType t]
typeArgsFor IMap (_ : _ : t : _) = [getBaseType t]
typeArgsFor _ _ = []

-- | Typecheck the application of an operator to some terms, returning
Expand Down

0 comments on commit 1735416

Please sign in to comment.