Nested hash generation error - crystal-lang

Given the following code:
require "big"
alias Type = Nil | String | Bool | Int32 | BigFloat | Array(Type) | Hash(String | Symbol, Type)
alias HOpts = Hash(String | Symbol, Type)
ctx = HOpts.new
ctx["test_int"] = 1
ctx["test_s"] = "hello"
c1 = Hash(String, Type).new
ctx["stuff"] = c1
ctx["stuff"]["foo"] = { "bar" => 1 }
I get:
Error in test.cr:13: instantiating 'Hash(String | Symbol, Type)#[]=(String, Hash(String, Type))'
ctx["stuff"] = c1
^
in /opt/crystal/src/hash.cr:43: instantiating 'insert_in_bucket(Int32, String, Hash(String, Type))'
entry = insert_in_bucket index, key, value
^~~~~~~~~~~~~~~~
in /opt/crystal/src/hash.cr:842: instantiating 'Hash::Entry(String | Symbol, Type)#value=(Hash(String, Type))'
entry.value = value
^~~~~
in /opt/crystal/src/hash.cr:881: expanding macro
property value : V
^
in macro 'property' expanded macro: macro_83313872:567, line 10:
1.
2.
3.
4. #value : V
5.
6. def value : V
7. #value
8. end
9.
> 10. def value=(#value : V)
11. end
12.
13.
14.
15.
instance variable '#value' of Hash::Entry(String | Symbol, Type) must be Type, not Hash(String, Type)
I would expect to be able to create any kind of nested hash but it does not work.

There's a couple of things wrong here.
The type of c1 is Hash(String, Type) which is not one of the types of the Type union. Hash(String, Type) is not compatible with Hash(String | Symbol, Type).
Either include Hash(String, Type) in the Type union, or give c1 the type Hash(String | Symbol, Type) (i.e. HOpts):
c1 = HOpts.new
You will also have another error on this line of code:
ctx["stuff"]["foo"] = { "bar" => 1 }
ctx["stuff"] will return an object of type Type and not a hash as you expect. If you know for certain that ctx["stuff"] is a hash (which we do from this example) then you need to restrict its type. Also { "bar" => 1 } is of type Hash(String, Int32) and not Hash(String, Type), so you need to specify this too:
ctx["stuff"].as(HOpts)["foo"] = HOpts{ "bar" => 1 }

Related

How to make an array of a composed type in Crystal?

I was trying
class Output
alias Type = String | Array(Output) | Hash(Symbol, Output)
getter raw
def initialize(#raw : Type)
end
end
hash = Output.new({ :a => Output.new("1") })
array = Output.new([hash.raw])
Type includes Array(Output) so I needed to pass the output and not the raw type:
array = Output.new([hash])

ANTLR - How to extact units from a dimension

I'm using ANTLR4 and the CSS grammar from https://github.com/antlr/grammars-v4/tree/master/css3. The grammar defines the following ( pared down a little for brevity )
dimension
: ( Plus | Minus )? Dimension
;
fragment FontRelative
: Number E M
| Number E X
| Number C H
| Number R E M
;
fragment AbsLength
: Number P X
| Number C M
| Number M M
| Number I N
| Number P T
| Number P C
| Number Q
;
fragment Angle
: Number D E G
| Number R A D
| Number G R A D
| Number T U R N
;
fragment Length
: AbsLength
| FontRelative
;
Dimension
: Length
| Angle
;
The matching works fine but I don't see an obvious way to extract the units. The parser creates a DimensionContext which has 3 TerminalNode members - Dimension, Plus and Minus. I'd like to be able to extract the unit during parse without having to do additional string parsing.
I know that one issue that the Length and Angle are fragments. I changed the grammar not use fragments
Unit
: 'em'
| 'ex'
| 'ch'
| 'rem'
| 'vw'
| 'vh'
| 'vmin'
| 'vmax'
| 'px'
| 'cm'
| 'mm'
| 'in'
| 'pt'
| 'q'
| 'deg'
| 'rad'
| 'grad'
| 'turn'
| 'ms'
| 's'
| 'hz'
| 'khz'
;
Dimension : Number Unit;
And things still parse but I don't get any more context about what the units are - the Dimension is still a single TerminalNode. Is there a way to deal with this without having to pull apart the full token string?
You will want to do as little as possible in the lexer:
NUMBER
: Dash? Dot Digit+ { atNumber(); }
| Dash? Digit+ ( Dot Digit* )? { atNumber(); }
;
UNIT
: { aftNumber() }?
( 'px' | 'cm' | 'mm' | 'in'
| 'pt' | 'pc' | 'em' | 'ex'
| 'deg' | 'rad' | 'grad' | '%'
| 'ms' | 's' | 'hz' | 'khz'
)
;
The trick is to produce the NUMBER and UNIT as separate tokens, yet limited to the required ordering. The actions in the NUMBER rule just set a flag and the UNIT predicate ensures that a UNIT can only follow a NUMBER:
protected void atNumber() {
_number = true;
}
protected boolean aftNumber() {
if (_number && Character.isWhitespace(_input.LA(1))) return false;
if (!_number) return false;
_number = false;
return true;
}
The parser rule is trivial, but preserves the detail required:
number
: NUMBER UNIT?
;
Use a tree-walk, parse the NUMBER to a Double and an enum (or equivalent) to provide the semantic UNIT characterization:
public enum Unit {
CM("cm", true, true), // 1cm = 96px/2.54
MM("mm", true, true),
IN("in", true, true), // 1in = 2.54cm = 96px
PX("px", true, true), // 1px = 1/96th
PT("pt", true, true), // 1pt = 1/72th
EM("em", false, true), // element font size
REM("rem", false, true), // root element font size
EX("ex", true, true), // element font x-height
CAP("cap", true, true), // element font nominal capital letters height
PER("%", false, true),
DEG("deg", true, false),
RAD("rad", true, false),
GRAD("grad", true, false),
MS("ms", true, false),
S("s", true, false),
HZ("hz", true, false),
KHZ("khz", true, false),
NONE(Strings.EMPTY, true, false), // 'no unit specified'
INVALID(Strings.UNKNOWN, true, false);
public final String symbol;
public final boolean abs;
public final boolean len;
private Unit(String symbol, boolean abs, boolean len) {
this.symbol = symbol;
this.abs = abs;
this.len = len;
}
public boolean isAbsolute() { return abs; }
public boolean isLengthUnit() { return len; }
// call from the visitor to resolve from `UNIT` to Unit
public static Unit find(TerminalNode node) {
if (node == null) return NONE;
for (Unit unit : values()) {
if (unit.symbol.equalsIgnoreCase(node.getText())) return unit;
}
return INVALID;
}
#Override
public String toString() {
return symbol;
}
}

How are declarations in a SML program separated?

In SML's grammar
Programs
prog ::= dec core declaration
functor fctbind functor declaration
signature sigbind signature declaration
empty
prog1 ⟨;⟩ prog2 sequence
fctbind ::= id1 ( id2 : sig ) ⟨:⟨>⟩ sig⟩ = str ⟨and fctbind⟩ plain
id ( spec ) ⟨:⟨>⟩ sig⟩ = str ⟨and fctbind⟩ opened
sigbind ::= id = sig ⟨and sigbind⟩ signature
Why
val a = 1
val b = 2;
a
b
has error between a and b, but not between the two val declarations?
$sml < main.sml
Standard ML of New Jersey v110.78 [built: Thu Aug 31 03:45:42 2017]
- val a = 1 : int
val b = 2 : int
= stdIn:4.1-5.2 Error: operator is not a function [tycon mismatch]
operator: int
in expression:
a b
Thanks.
In
val a = 1
val b = 2
All you've done is bind two variables, corresponding to the rules
dec ::= val ⟨var⟩(,) valbind, dec ::= dec1 ⟨;⟩ dec2, and at top level,
prog ::= dec.
Then in
a
b
You're actually attempting to apply a to b (exp ::= exp1 exp2 (application)). It might be simpler to see it as the equivalently written
a b
However, a doesn't have a function type, hence the error operator is not a function. It's unclear what you're actually attempting to do with a and b.

What is the difference between JSON::Any and JSON::Type in Crystal?

In Crystal language, what is the difference between JSON::Any and JSON::Type? What are the use cases of this types?
JSON::Any is a struct, which is returned as a result of parsing. It has convenient methods to access underlying data as_s, as_bool, as_f etc. :
obj = JSON.parse %({"access": true})
p obj.class # => JSON::Any
p obj["access"] # => true
p obj["access"].class # => JSON::Any
JSON::Type is an union type of all possible json types. It is used internally by JSON::Any struct to represent the data:
p obj.raw # => {"access" => true}
p obj.raw.class # => Hash(String, JSON::Type)
JSON::Type is a recursively-defined "alias":
alias Type = Nil | Bool | Int64 | Float64 | String | Array(Type) | Hash(String, Type)
Aliases are part of Crystal's type grammar. For details, see https://crystal-lang.org/docs/syntax_and_semantics/alias.html
JSON::Any is a Struct (Struct < Value < Object); an instance of JSON::Any holds the "raw" value of any JSON type:
cr(0.24.1) > x=JSON::Any.new("hi")
=> "hi"
icr(0.24.1) > x
=> "hi"
icr(0.24.1) > x.raw
=> "hi"

Generic Hashes as arguments?

I'm trying to implement a class which accepts a generic payload and then converts it to JSON:
require "json"
class Response
alias Payload = Hash(Symbol | String, String | Bool | Int32 | Int64 | Float32 | Float64 | Nil)
#payload : Payload
def initialize(#payload, #method : String = "sendMessage")
end
def to_json
#payload.merge({"method" => #method}).to_json
end
end
Response.new({
"chat_id" => update.message.not_nil!.chat.id,
"text" => "Crystal is awesome but complicated",
})
But I get instance variable '#payload' of Response must be Hash(String | Symbol, Bool | Float32 | Float64 | Int32 | Int64 | String | Nil), not Hash(String, Int64 | String) compiler error. How can I overcome this? Does Crystal support generic Hashes? Why it can't convert even if types overlap?
The Response is a part of a shard, so I don't know which hashes would be passed as arguments, but Payload has to be enough.
Your payload hash is of type Hash(String, Int32 | String):
typeof({
"chat_id" => update.message.not_nil!.chat.id,
"text" => "Crystal is awesome but complicated",
}) # => Hash(String, Int32 | String)
But the constructor expects a Hash(Symbol | String, String | Bool | Int32 | Int64 | Float32 | Float64 | Nil).
These are diffferent types and cannot be magically casted. You'll need to ensure your payload has the correct type.
One way to do this is to explicitly declare the type of the hash literal:
Payload{
"chat_id" => update.message.not_nil!.chat.id,
"text" => "Crystal is awesome but complicated",
}
This is of course not great, but depending on your usecase it might be sufficient.
If you want to have a general interface that allows to receive any type of hash, you'll need to cast it to the Payload type. This means copying the data into a new hash of that type:
def self.new(hash, method : String = "sendMessage")
payload = Payload.new
hash.each do |key, value|
payload[key] = value
end
new(payload, method)
end
For a real-world example, I am using this approach in Crinja to convert a number of different type variations to the matching ones.