# New Ticket Created by  Kodi Arfer 
# Please include the string:  [perl #77896]
# in the subject line of all future correspondence about this issue. 
# <URL: http://rt.perl.org/rt3/Ticket/Display.html?id=77896 >


This patch adds a bunch of container types and lets you do 'my %h is
SomeType'. See the commit message for the details.
From a010550295d5ef9392e5cc0e60dd120fb0fe79e8 Mon Sep 17 00:00:00 2001
From: Kodi Arfer <hi...@thoth.(none)>
Date: Thu, 16 Sep 2010 09:14:23 -0400
Subject: [PATCH] Containers galore.

I added the Bag, KeyHash, KeySet, and KeyBag types and modified Set so
that it can pass S02-builtin_data_types/set.t. I also edited
Actions.pm so you can say 'my %h is SomeType', although this only
works for variables with the % sigil, and the type has to be just a
type name, so for now if you want to say 'my %h is KeyHash[Int, 42]'
you have to instead say 'role R does KeyHash[Int, 42]; my %h is R'.

To make it possible to implement KeyHashes, I implemented a crude
means of overloading &infix:<=>: if you say '$c = $v' and the
container of $c has a PMC property "assignment_trapper" with value $t,
then &infix:<=> calls '$t.ASSIGN($v)' instead of the 'setref' opcode.

I spun the weighted-picking logic in Hash off into its own role,
WeightedPick, so Bag could use it. We can reverse this once hashes
permit non-string keys, in which case Bag (along with Set) can be
reimplemented with a hash.

While I was writing a patch, I fixed a bug in Duration.pm that was
exposed when masak++ made the Instant class do Real.
---
 build/Makefile.in       |    4 +-
 docs/ChangeLog          |    2 +
 docs/ROADMAP            |    3 +-
 src/Perl6/Actions.pm    |   12 ++++-
 src/builtins/assign.pir |    8 +++
 src/core/Bag.pm         |   59 +++++++++++++++++++++++++
 src/core/Duration.pm    |    3 -
 src/core/Hash.pm        |  112 ++++++++++++++++++++--------------------------
 src/core/KeyHash.pm     |  104 +++++++++++++++++++++++++++++++++++++++++++
 src/core/Set.pm         |   24 ++++++++--
 t/spectest.data         |    8 +++
 11 files changed, 264 insertions(+), 75 deletions(-)
 create mode 100644 src/core/Bag.pm
 create mode 100644 src/core/KeyHash.pm

diff --git a/build/Makefile.in b/build/Makefile.in
index f86bc9e..5e20f97 100644
--- a/build/Makefile.in
+++ b/build/Makefile.in
@@ -191,7 +191,6 @@ CORE_SOURCES = \
   src/core/Cool-num.pm \
   src/core/Cool-str.pm \
   src/core/List.pm \
-  src/core/Set.pm \
   src/core/Array.pm \
   src/core/Numeric.pm \
   src/core/Real.pm \
@@ -210,6 +209,9 @@ CORE_SOURCES = \
   src/core/Range.pm \
   src/core/EnumMap.pm \
   src/core/Hash.pm \
+  src/core/Set.pm \
+  src/core/Bag.pm \
+  src/core/KeyHash.pm \
   src/core/Enum.pm \
   src/core/IO.pm \
   src/core/IO/ArgFiles.pm \
diff --git a/docs/ChangeLog b/docs/ChangeLog
index 5806310..94eb888 100644
--- a/docs/ChangeLog
+++ b/docs/ChangeLog
@@ -3,6 +3,8 @@
 + Instants and Durations
 + speedup for slurp() and .reverse built-ins
 + various improvements to the Set type
++ add Bag, KeyHash, KeySet, KeyBag, KeyWeight
++ permit setting the implementation type of a %var with 'is' ('my %h is KeyBag')
 + revamp of series operator code, and adaption to new spec
 + implement ...^ up-to-but-excluding-series operator
 + allow :r and :ratchet modifiers on regex quoting constructs
diff --git a/docs/ROADMAP b/docs/ROADMAP
index e6474e1..4913e4f 100644
--- a/docs/ROADMAP
+++ b/docs/ROADMAP
@@ -34,7 +34,7 @@ Ought to have items
 2 **    Rat, BigNum, numification improvements (C, colomon)
 2 ***   temp variables (C)
 2 ****  better longest token matching in regexes (A, D, pmichaud)
-2 ?     other S02 data types -- KeySet, KeyBag (A)
+2 ?     other S02 data types (A)
 2 ?     specialized Unicode bits -- .codes, .graphs, .bytes (A, C)
 
 Nice to have items
@@ -115,3 +115,4 @@ Completed ROADMAP items:
   - true hyper/cross/reverse/other metaoperators
   - synopsis 19 handling
   - other REPL improvements
+  - KeyHash and derivatives
diff --git a/src/Perl6/Actions.pm b/src/Perl6/Actions.pm
index 99f8231..efccf60 100644
--- a/src/Perl6/Actions.pm
+++ b/src/Perl6/Actions.pm
@@ -1049,9 +1049,15 @@ sub declare_variable($/, $past, $sigil, $twigil, $desigilname, $trait_list) {
     else {
         # Not an attribute - need to emit delcaration here.
         # Create the container
-        my $cont := $sigil eq '%' ??
-            PAST::Op.new( :name('&CREATE_HASH_FROM_LOW_LEVEL'), :pasttype('call') ) !!
-            PAST::Op.new( sigiltype($sigil), :pirop('new Ps') );
+        my $cont;
+        if $sigil eq '%' {
+            my $itype := has_compiler_trait($trait_list, '&trait_mod:<is>');
+            $cont := $itype
+               ?? PAST::Op.new(:pasttype('callmethod'), :name('new'), $itype[0])
+               !! PAST::Op.new( :name('&CREATE_HASH_FROM_LOW_LEVEL'), :pasttype('call') );
+        } else {
+            $cont := PAST::Op.new( sigiltype($sigil), :pirop('new Ps') );
+        }
         
         # Give it a 'rw' property unless it's explicitly readonly.
         my $readtype := trait_readtype($trait_list);
diff --git a/src/builtins/assign.pir b/src/builtins/assign.pir
index d462c4c..9963895 100644
--- a/src/builtins/assign.pir
+++ b/src/builtins/assign.pir
@@ -66,6 +66,11 @@ src/builtins/assign.pir - assignment operations
     whence()
 
   scalar_assign:
+    .local pmc trapper
+    trapper = getprop 'assignment_trapper', cont
+    if null trapper goto check_nil_assign
+    goto item_assign
+  check_nil_assign:
     # check for Nil assignment
     $I0 = isa source, ['Nil']
     unless $I0 goto item_assign
@@ -81,6 +86,9 @@ src/builtins/assign.pir - assignment operations
     source = source.'item'()
   have_source:
     source = descalarref source
+    if null trapper goto normal_assignment
+    .tailcall trapper.'ASSIGN'(source)
+  normal_assignment:
     setref cont, source
     .return (cont)
 
diff --git a/src/core/Bag.pm b/src/core/Bag.pm
new file mode 100644
index 0000000..9eff052
--- /dev/null
+++ b/src/core/Bag.pm
@@ -0,0 +1,59 @@
+use v6;
+
+class Bag does Associative does WeightedPick {
+    # We could use a hash here, but right now hash keys coerce to Str,
+    # so instead let's use an array of Pairs for the time being.
+    has @!p;
+
+    multi readonly(@a) { @a }
+    multi readonly($x) { $x }
+
+    multi method new()       { self.bless: * }
+
+    multi method new(@elems) {
+        my @p;
+        for @elems -> $e {
+            if @p.grep(*.key eqv $e)[0] {
+                ++$^p.value;
+            } else {
+                push @p, $e => 1;
+            }
+        }
+        self.bless: *, p => @p;
+    }
+
+    multi method new(*...@elems) { self.new: @elems }
+
+    multi method new(Bag $b) { $b }
+
+    multi method new(Set $s) { self.new: $s.keys }
+
+    method !STORE(\$args) {
+        die "Bags are immutable, but you tried to modify one"
+    }
+
+    method at_key($k) { 
+        .key eqv $k and return readonly .value for @!p;
+        0;
+    }
+
+    method keys()     { readonly @!p>>.key }
+    method values()   { readonly @!p>>.value }
+    method pairs()    { readonly @!p }
+    method elems()    { [+] self.values }
+    method exists($k) { ? do self.keys.grep: * eqv $k }
+
+    method Bool()     { ?...@!p }
+    method Numeric()  { self.elems }
+    method Str()      { self.perl }
+    method hash()     { @!p.hash }
+    method flat()     { @!p.flat }
+
+    method perl() {
+        'bag(' ~ self.list>>.perl.join(', ') ~ ')';
+    }
+}
+
+our sub bag(*...@args) { Bag.new: |@args }
+
+# vim: ft=perl6
diff --git a/src/core/Duration.pm b/src/core/Duration.pm
index 97effe2..3f98ab7 100644
--- a/src/core/Duration.pm
+++ b/src/core/Duration.pm
@@ -30,9 +30,6 @@ our multi sub infix:<+>(Duration $a, Duration $b) {
 our multi sub infix:<->(Duration $a, Real $b) {
     Duration.new: $a.x - $b;
 }
-our multi sub infix:<->(Real $a, Duration $b) {
-    Duration.new: $a - $b.x;
-}
 our multi sub infix:<->(Duration $a, Duration $b) {
     Duration.new: $a.x - $b.x;
 }
diff --git a/src/core/Hash.pm b/src/core/Hash.pm
index b4e135c..1f1661a 100644
--- a/src/core/Hash.pm
+++ b/src/core/Hash.pm
@@ -1,4 +1,52 @@
-role Hash is EnumMap {
+use v6;
+
+role WeightedPick {
+
+    sub weighted-pick(@pairs) {
+        my @weights = [\+] @pairs>>.value;
+        my $value = @weights[*-1].rand;
+        return @pairs[0] if @weights[0] > $value;
+        my ($l, $r) = (0, @weights.end);
+        my $middle = floor ($r + $l) / 2;
+        while $middle > $l {
+            if @weights[$middle] < $value {
+                $l = $middle;
+            }
+            else {
+                 $r = $middle;
+            }
+            $middle = floor ($r + $l) / 2;
+        }
+        @pairs[$r];
+    }
+
+    multi method pick() { weighted-pick(self.pairs).key }
+
+    multi method pick($n is copy) {
+        $n <= 0 and return Nil;
+        my @p = self.pairs>>.clone.grep: *.value;
+        gather {
+            while $n-- && @p {
+                my $pp = weighted-pick @p;
+                take $pp.key;
+                --$pp.value or @p .= grep: {.key !eqv $pp.key};
+            }
+        }
+    }
+
+    multi method pick(Whatever) { self.pick: Inf }
+
+    multi method roll($n is copy = 1) {
+       gather {
+            take self.pick for ^$n;
+       }
+    }
+
+    multi method roll(Whatever) { self.roll: Inf }
+
+}
+
+role Hash is EnumMap does WeightedPick {
     method at_key($key) {
         my $z = Any!butWHENCE(
                     { pir::set__vQsP($!storage, $key, $z); }
@@ -97,68 +145,6 @@ role Hash is EnumMap {
         self.pairs.sort(&by)
     }
 
-    multi method pick($num is copy = 1) {
-        if ($num == 1) {
-            my @weights = [\+] self.values;
-            my $value = @weights[*-1].rand;
-            return self.keys[0] if @weights[0] > $value;
-            my ($l, $r) = (0, @weights.elems-1);
-            my $middle = floor ($r + $l) / 2;
-            while $middle > $l {
-                if @weights[$middle] < $value {
-                    $l = $middle;
-                }
-                else {
-                     $r = $middle;
-                }
-                $middle = floor ($r + $l) / 2;
-            }
-            return self.keys[$r];
-        }
-
-        my %copyHash = @.pairs.grep({ .value != 0});
-        gather {
-            while $num > 0 && %copyHash {
-                take my $picked = %copyHash.pick();
-                unless --%copyHash{$picked} {
-                    %copyHash.delete($picked);
-                }
-                $num--;
-            }
-        }
-    }
-
-    multi method pick(Whatever) {
-        self.pick(Inf);
-    }
-
-    multi method roll($num is copy = 1) {
-        if ($num == 1) {
-            my @weights = [\+] self.values;
-            my $value = @weights[*-1].rand;
-            return self.keys[0] if @weights[0] > $value;
-            my ($l, $r) = (0, @weights.elems-1);
-            my $middle = floor ($r + $l) / 2;
-            while $middle > $l {
-                if @weights[$middle] < $value {
-                    $l = $middle;
-                }
-                else {
-                     $r = $middle;
-                }
-                $middle = floor ($r + $l) / 2;
-            }
-            return self.keys[$r];
-        }
-
-        gather {
-            take self.roll() for ^$num;
-        }
-    }
-
-    multi method roll(Whatever) {
-        self.roll(Inf);
-    }
 }
 
 
diff --git a/src/core/KeyHash.pm b/src/core/KeyHash.pm
new file mode 100644
index 0000000..3374f5d
--- /dev/null
+++ b/src/core/KeyHash.pm
@@ -0,0 +1,104 @@
+use v6;
+
+class KeyHashTrapper {
+
+    has $.kh;
+    has $.key;
+
+    # # This fails with errors that change from run to run—perhaps
+    # # because of a bug in Hash.pm.
+    # method ASSIGN($val) {
+    #     $.kh.okay-val($val)
+    #       ?? ($.kh.Hash::at_key($.key) = $val)
+    #       !! $.kh.Hash::delete($.key);
+    #     $.kh{$.key};
+    # }
+
+    method ASSIGN($val) { Q:PIR {
+        .local pmc self
+        self = find_lex 'self'
+        .local pmc kh
+        kh = getattribute self, '$!kh'
+        .local pmc store
+        store = getattribute kh, '$!storage'
+        .local pmc key
+        key = getattribute self, '$!key'
+        .local pmc val
+        val = find_lex '$val'
+
+        $P0 = kh.'okay-val'(val)
+        if $P0 goto set
+        delete store[key]
+        goto done
+      set:
+        store[key] = val
+      done:
+        %r = kh.'at_key'(key)
+    } }
+
+}
+
+class DefaultDefault {};
+
+role KeyHash[::T = Any, $default = DefaultDefault] does Hash {
+
+    has $!default-value = $default ~~ DefaultDefault
+      ?? do { given T {
+          when Bool       { False }
+          when Num        { 0.Num }
+          when Rat        { 0.Rat }
+          when Numeric    { 0 }
+          when Stringy    { '' }
+          default         { T }
+        } }
+      !! $default;
+
+    method !get($key) { Q:PIR {
+        $P0 = find_lex 'self'
+        $P1 = getattribute $P0, '$!storage'
+        $P0 = find_lex '$key'
+        %r = $P1[$P0]
+    } }
+
+    method at_key($key) {
+        my T $subscript = self.exists($key)
+          ?? self!get: $key
+          !! $!default-value;
+        pir::setprop__vPsP($subscript, 'assignment_trapper',
+            KeyHashTrapper.new: kh => self, :$key);
+        $subscript;
+    }
+
+    method okay-val($v) {
+        $default ~~ DefaultDefault ?? ?$v !! $v !eqv $default
+    }
+
+    method elems() { [+] self.values }    
+
+    method grab(Int $n is copy = 1) {
+        gather {
+            while $n-- && self.keys {
+                take my $picked = self.pick;
+                --self.{$picked} or self.delete: $picked;
+            }
+        }
+    }
+
+    method Numeric() { self.elems }
+
+    method perl() { sprintf 'KeyHash[%s, %s].new(%s)',
+        T.perl,
+        $!default-value.perl,
+        self.pairs.map(*.perl).join(', ')
+    }
+
+}
+
+class KeySet does KeyHash[Bool] { }
+
+class KeyBag does KeyHash[Int] {
+    # Without real UInts, we have to cheat a bit.
+    method okay-val($v) { $v > 0 }
+}
+
+class KeyWeight does KeyHash[Rat] { } # XXX Should be FatRat
diff --git a/src/core/Set.pm b/src/core/Set.pm
index ac3d26e..85e74a0 100644
--- a/src/core/Set.pm
+++ b/src/core/Set.pm
@@ -1,8 +1,13 @@
+use v6;
+
 class Set does Associative {
     # We could use a hash here, but right now hash keys coerce to Str,
     # so instead let's use an array and &uniq for the time being.
     has @!elems;
 
+    multi method new() {
+        self.bless: *;
+    }
     multi method new(@elems) {
         self.bless(self.CREATE, :elems( uniq @elems ));
     }
@@ -16,6 +21,10 @@ class Set does Associative {
         $set;
     }
 
+    method !STORE(\$args) {
+        die 'Sets are immutable, but you tried to modify one'
+    }
+
     sub contains(@array, $value) {
         for @array {
             if $value === $_ {
@@ -25,7 +34,7 @@ class Set does Associative {
         return False;
     }
 
-    method keys() { @!elems }
+    method keys() { { @^readonly-elems }(@!elems) }
     method values() { True xx +...@!elems }
     method elems() { +...@!elems }
     method exists($elem) { contains(@!elems, $elem) }
@@ -34,9 +43,14 @@ class Set does Associative {
         contains(@!elems, $key);
     }
 
-    method Num() { +self.elems }
     method Bool() { ?self.elems }
-    method hash() { hash @!elems Z=> True xx * }
+    method Numeric() { +self.elems }
+    method Str() { self.perl }
+    method hash() { hash self.flat }
+    method flat() { @!elems Z=> True xx * }
+
+    method pick(*...@args) { @!elems.pick: |@args }
+    method roll(*...@args) { @!elems.roll: |@args }
 
     multi method union(@otherset) {
         self.new((@!elems, @otherset));
@@ -93,7 +107,7 @@ class Set does Associative {
     }
 
     method perl() {
-        'Set.new(' ~ join(', ', map { .perl }, @!elems) ~ ')';
+        'set(' ~ join(', ', map { .perl }, @!elems) ~ ')';
     }
 }
 
@@ -137,4 +151,6 @@ our multi sub  infix:«(>)»(    %a, %b) { Set.new( %a).superset(%b) }
 our multi sub  infix:«(>)»(    @a, %b) { Set.new(|@a).superset(%b) }
 our multi sub  infix:«(>)»(    @a, @b) { Set.new(|@a).superset(@b) }
 
+our sub set(*...@args) { Set.new: |@args }
+
 # vim: ft=perl6
diff --git a/t/spectest.data b/t/spectest.data
index c84a7b0..3bbd41a 100644
--- a/t/spectest.data
+++ b/t/spectest.data
@@ -68,6 +68,7 @@ S02-builtin_data_types/array_extending.t
 S02-builtin_data_types/array_ref.t
 S02-builtin_data_types/array.t
 S02-builtin_data_types/assigning-refs.t
+S02-builtin_data_types/bag.t
 S02-builtin_data_types/bool.t
 S02-builtin_data_types/capture.t
 S02-builtin_data_types/catch_type_cast_mismatch.t
@@ -76,6 +77,11 @@ S02-builtin_data_types/flattening.t
 S02-builtin_data_types/hash_ref.t
 S02-builtin_data_types/hash.t
 S02-builtin_data_types/infinity.t
+S02-builtin_data_types/instants-and-durations.t
+S02-builtin_data_types/keybag.t
+S02-builtin_data_types/keyhash.t
+S02-builtin_data_types/keyset.t
+S02-builtin_data_types/keyweight.t
 S02-builtin_data_types/lists.t
 S02-builtin_data_types/mixed_multi_dimensional.t
 S02-builtin_data_types/multi_dimensional_array.t
@@ -88,6 +94,7 @@ S02-builtin_data_types/pair.t
 S02-builtin_data_types/parcel.t
 S02-builtin_data_types/parsing-bool.t
 S02-builtin_data_types/range.t
+S02-builtin_data_types/set.t
 S02-builtin_data_types/sigils-and-types.t
 S02-builtin_data_types/subscripts_and_context.t
 S02-builtin_data_types/type.t
@@ -596,6 +603,7 @@ S32-str/words.t                                            # icu
 S32-temporal/calendar.t
 S32-temporal/Date.t
 S32-temporal/DateTime.t
+S32-temporal/DateTime-Instant-Duration.t
 S32-trig/e.t
 # S32-trig/pi.t
 S32-trig/sin.t                                             # long
-- 
1.7.0.4

Reply via email to