├── test ├── todo_examples │ └── sum_lines │ │ ├── out1.txt │ │ ├── prog.a2d │ │ └── in1.txt ├── examples │ ├── duplicate_assignment.test │ ├── empty_char_error.test │ ├── div0_error.test │ ├── unterminated_string_error.test │ ├── int_columns.test │ ├── nested_invalid_ref.test │ ├── non_binary_error.test │ ├── unset_identifier_error.test │ ├── input_lines.test │ ├── comment.test │ ├── undefined_for_base.test │ ├── triple_use.test │ ├── utf8.test │ ├── t1.test │ ├── circular_vars2.test │ ├── multiple_type_errors.test │ ├── multiline_commands_and_strs.test │ ├── help.test │ ├── test_output_different_types.test │ └── large_prog.test ├── repl_test_input.txt ├── update_date.rb ├── test_repl.sh ├── all ├── test_op_examples.rb ├── repl_test_expected.txt ├── test_behavior.rb ├── check_example.rb ├── test_fuzz.rb ├── test_docs.rb ├── test_examples.rb ├── test_parse.rb ├── behavior_tests.atl └── test_vec.rb ├── atlas ├── docs ├── op_notes.md ├── io.md ├── vectorization.md ├── quickref.md ├── types.md ├── syntax.md └── circular.md ├── LICENSE ├── error.rb ├── type.rb ├── escape.rb ├── lex.rb ├── repl.rb ├── ir.rb ├── spec.rb ├── parse.rb ├── infer.rb ├── lazylib.rb ├── ops.rb └── README.md /test/todo_examples/sum_lines/out1.txt: -------------------------------------------------------------------------------- 1 | 6 2 | 15 -------------------------------------------------------------------------------- /test/todo_examples/sum_lines/prog.a2d: -------------------------------------------------------------------------------- 1 | 0,:+\!~ 2 | ].^ -------------------------------------------------------------------------------- /test/todo_examples/sum_lines/in1.txt: -------------------------------------------------------------------------------- 1 | 1 2 3 2 | 4 5 6 3 | -------------------------------------------------------------------------------- /atlas: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require_relative "repl.rb" 4 | 5 | repl 6 | -------------------------------------------------------------------------------- /test/examples/duplicate_assignment.test: -------------------------------------------------------------------------------- 1 | [prog] 2 | a=5 3 | a=3 4 | a 5 | 6 | [stdout] 7 | 3 -------------------------------------------------------------------------------- /test/examples/empty_char_error.test: -------------------------------------------------------------------------------- 1 | [prog] 2 | // 3 | ' 4 | [stderr] 5 | 2:3 (') empty char -------------------------------------------------------------------------------- /test/examples/div0_error.test: -------------------------------------------------------------------------------- 1 | [prog] 2 | 1/0 3 | 4 | [stderr] 5 | 1:2 (/) div 0 (DynamicError) 6 | -------------------------------------------------------------------------------- /test/examples/unterminated_string_error.test: -------------------------------------------------------------------------------- 1 | [prog] 2 | "hi 3 | [stderr] 4 | unterminated string (LexError) -------------------------------------------------------------------------------- /test/examples/int_columns.test: -------------------------------------------------------------------------------- 1 | [prog] 2 | }+} 3 | [input] 4 | 1 10 50 5 | 4 20 80 6 | [stdout] 7 | 11 8 | 24 9 | -------------------------------------------------------------------------------- /test/examples/nested_invalid_ref.test: -------------------------------------------------------------------------------- 1 | [prog] 2 | a=b 3 | a 4 | [stderr] 5 | 1:3 (b) unset identifier "b" (ParseError) 6 | -------------------------------------------------------------------------------- /test/examples/non_binary_error.test: -------------------------------------------------------------------------------- 1 | [prog] 2 | 1+ 3 | [stderr] 4 | 1:2 (+) op not defined for unary operations (ParseError) -------------------------------------------------------------------------------- /test/examples/unset_identifier_error.test: -------------------------------------------------------------------------------- 1 | [prog] 2 | asdf 3 | 4 | [stderr] 5 | 1:1 (asdf) unset identifier "asdf" (ParseError) 6 | -------------------------------------------------------------------------------- /test/examples/input_lines.test: -------------------------------------------------------------------------------- 1 | [input] 2 | 1 2 3 3 | 4 5 6 4 | 5 | [prog] 6 | $&\ 7 | [stdout] 8 | 1 4 9 | 2 5 10 | 3 6 11 | -------------------------------------------------------------------------------- /test/examples/comment.test: -------------------------------------------------------------------------------- 1 | [prog] 2 | -- ignored 3 | 1 -- ignored 4 | 2--ignored 5 | 3 6 | --ignored 7 | [stdout] 8 | 1 9 | 2 10 | 3 -------------------------------------------------------------------------------- /test/examples/undefined_for_base.test: -------------------------------------------------------------------------------- 1 | [prog] 2 | 'a + 'b 3 | 4 | [stderr] 5 | 1:4 (+) op is not definied for arg types: Char,Char (TypeError) -------------------------------------------------------------------------------- /test/repl_test_input.txt: -------------------------------------------------------------------------------- 1 | *4 -- That is an up arrow (prev command 1+2)followed by *4 2 | 1,2 tai -- Two tabs to complete tail 3 | 4 | 5 5 | a,z@a 6 | a 7 | -------------------------------------------------------------------------------- /test/update_date.rb: -------------------------------------------------------------------------------- 1 | s=File.read('Ops.rb') 2 | s.sub!(/Atlas Alpha \(.*?\)/) { "Atlas Alpha (#{Time.now.strftime("%b %d, %Y")})"} 3 | File.open('Ops.rb','w'){|f|f< /dev/null 2 | cat test/repl_test_input.txt | ruby atlas > test/repl_test_output.txt 2>&1 3 | diff test/repl_test_output.txt test/repl_test_expected.txt 4 | -------------------------------------------------------------------------------- /test/examples/multiple_type_errors.test: -------------------------------------------------------------------------------- 1 | [prog] 2 | (1+()) + (2+()) 3 | [stderr] 4 | 1:3 (+) op is not definied for arg types: Int,[A] (TypeError) 5 | 1:12 (+) op is not definied for arg types: Int,[A] (TypeError) -------------------------------------------------------------------------------- /test/examples/multiline_commands_and_strs.test: -------------------------------------------------------------------------------- 1 | [prog] 2 | 1+ 3 | --asdf 4 | 5 | 2 6 | "ab 7 | cd"p 8 | 1/0 9 | 10 | 11 | 5 12 | [stdout] 13 | 3 14 | "ab\ncd" 15 | [stderr] 16 | 7:2 (/) div 0 (DynamicError) -------------------------------------------------------------------------------- /docs/op_notes.md: -------------------------------------------------------------------------------- 1 | # todo auto generate 2 | 3 | This will contain information on the few ops that aren't completely obvious/trivial. 4 | 5 | for now just type the op you are interested in by itself into the atlas repl to see all desc/test cases for it -------------------------------------------------------------------------------- /test/all: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ruby test/test_parse.rb 3 | ruby test/test_vec.rb 4 | ruby test/test_behavior.rb 5 | ruby test/test_examples.rb 6 | ruby test/test_op_examples.rb 7 | ruby test/test_docs.rb 8 | sh test/test_repl.sh 9 | ruby test/update_date.rb 10 | -------------------------------------------------------------------------------- /test/test_op_examples.rb: -------------------------------------------------------------------------------- 1 | require './test/check_example.rb' 2 | 3 | pass=0 4 | OpsList.each{|op| 5 | (op.examples+op.tests).each{|example| 6 | check_example(example){ 7 | "example test for op: #{op.name}" 8 | } 9 | pass+=1 10 | } 11 | } 12 | puts "PASS #{pass} op examples" 13 | -------------------------------------------------------------------------------- /test/repl_test_expected.txt: -------------------------------------------------------------------------------- 1 |  ᐳ 1+2*4 -- That is an up arrow (prev command 1+2)followed by *4 2 |  ᐳ 1,2 tail -- Two tabs to complete tail 3 |  ᐳ  4 |  ᐳ 5 5 |  ᐳ a,z@a 6 | 5:3 (z) unset identifier "z" (ParseError) 7 |  ᐳ a 8 | 6:1 (a) unset identifier "a" (ParseError) 9 |  ᐳ 12 10 | 2 11 | 5 12 | -------------------------------------------------------------------------------- /test/test_behavior.rb: -------------------------------------------------------------------------------- 1 | require "./test/check_example.rb" 2 | 3 | line = 1 4 | pass = 0 5 | behavior_tests = File.read("./test/behavior_tests.atl").lines.to_a 6 | behavior_tests.map{|test| 7 | (line+=1; next) if test.strip == "" || test =~ /^--/ 8 | check_example(test){ 9 | "behavior test line %d" % [line] 10 | } 11 | pass += 1 12 | line += 1 13 | } 14 | 15 | puts "PASS %d behavior tests" % pass 16 | -------------------------------------------------------------------------------- /test/examples/help.test: -------------------------------------------------------------------------------- 1 | [prog] 2 | . 3 | % 4 | add 5 | - 6 | [stdout] 7 | vectorize . 8 | [a] → 9 | 1,2,3. → <1,2,3> 10 | 11 | mod % 12 | Int Int → Int 13 | 7%3 → 1 14 | 10%5 → 0 15 | 9%5 → 4 16 | 11%(5-) → -4 17 | 10%(5-) → 0 18 | 11-%5 → 4 19 | 10-%5 → 0 20 | 10-%(5-) → 0 21 | 9-%(5-) → -4 22 | 5%0 → DynamicError 23 | 24 | unvec % 25 | → [a] 26 | 1,2+3% → [4,5] 27 | 28 | add + 29 | Int Int → Int 30 | Int Char → Char 31 | Char Int → Char 32 | 1+2 → 3 33 | 'a+1 → 'b 34 | 35 | sub - 36 | Int Int → Int 37 | Char Int → Char 38 | Char Char → Int 39 | 5-3 → 2 40 | 41 | neg - 42 | Int → Int 43 | 2- → -2 44 | -------------------------------------------------------------------------------- /test/examples/test_output_different_types.test: -------------------------------------------------------------------------------- 1 | [prog] 2 | "int" 3 | 3 4 | "(int)" 5 | 0,1,3 6 | "str" 7 | "asdf" 8 | "chr" 9 | 'a 10 | "(str)" 11 | "a1","b2","c" 12 | "((int))" 13 | 1,3,(1,4) 14 | "(((int))" 15 | 1,3,(1,4),(1,3,(1,4);) 16 | "((((int))))" 17 | 1,3,(1,4),(1,3,(1,4);),(1,3,(1,4),(1,3,(1,4);)) 18 | "" 19 | "((str))" 20 | "a1","b2","c"@v1,v1 21 | "(((str)))" 22 | v1,v1@v2,v2 23 | 24 | 25 | [stdout] 26 | int 27 | 3 28 | (int) 29 | 0 1 3 30 | str 31 | asdf 32 | chr 33 | a 34 | (str) 35 | a1 b2 c 36 | ((int)) 37 | 1 3 38 | 1 4 39 | (((int)) 40 | 1 3 41 | 1 4 42 | 43 | 1 3 44 | 1 4 45 | ((((int)))) 46 | 1 3 47 | 1 4 48 | 49 | 1 3 50 | 1 4 51 | 52 | 1 3 53 | 1 4 54 | 55 | 1 3 56 | 1 4 57 | ((str)) 58 | a1 b2 c 59 | a1 b2 c 60 | (((str))) 61 | a1 b2 c 62 | a1 b2 c 63 | 64 | a1 b2 c 65 | a1 b2 c -------------------------------------------------------------------------------- /test/check_example.rb: -------------------------------------------------------------------------------- 1 | require "./repl.rb" 2 | require 'stringio' 3 | 4 | def check_example(test) 5 | i,o=test.split("->") 6 | o.strip! 7 | 8 | expected = o 9 | expected,limit = if expected =~ /\.\.\.$/ 10 | [$`,$`.size] 11 | else 12 | [expected,10000] 13 | end 14 | 15 | begin 16 | found,found_type = doit(i, limit) 17 | rescue Exception 18 | found = $! 19 | end 20 | 21 | if o=~/Error/ ? found.class.to_s!=o : found != expected 22 | STDERR.puts "FAIL: "+yield 23 | STDERR.puts i 24 | STDERR.puts "expected:" 25 | STDERR.puts expected 26 | raise found if Exception === found 27 | STDERR.puts "found:" 28 | STDERR.puts found 29 | exit(1) 30 | end 31 | end 32 | 33 | def doit(source,limit) 34 | tokens,lines = lex(source) 35 | root = parse_line(tokens[0],[]) 36 | root_ir = to_ir(root,{}) 37 | ir = IR.new(Ops1['show'],[root_ir]) 38 | infer(ir) 39 | 40 | output = StringIO.new 41 | run(ir, output, limit) 42 | [output.string,root_ir.type.inspect] 43 | end -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Darren Smith 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/test_fuzz.rb: -------------------------------------------------------------------------------- 1 | require 'stringio' 2 | require "./repl.rb" 3 | 4 | symbols = "~`!@#$%^&*()_-+={[}]|\\'\";:,<.>/?" 5 | 6 | 7 | #symbols = "~!!@@$()-=[]';?:" 8 | numbers = "0123" 9 | letters = "abC" 10 | spaces = " \n\n\n\n" # twice as likely 11 | 12 | # Just the interesting characters to focus on testing parse 13 | # all = "[! \n()'\"1\\:?ab".chars.to_a + [':=','a:=',"seeParse","seeInference","seeType"] 14 | 15 | all = (symbols+numbers+letters+spaces).chars+['"ab12"'] 16 | 17 | ReadStdin = Promise.new{ str_to_lazy_list("ab12") } 18 | 19 | # todo take all tests and make larger programs that are almost correct 20 | 21 | n = 1000000 22 | step_limit = 1000 23 | 24 | 4.upto(8){|program_size| 25 | n.times{ 26 | program = program_size.times.map{all[(rand*all.size).to_i]}*"" 27 | program_io=StringIO.new(program) 28 | output_io=StringIO.new 29 | begin 30 | puts program 31 | repl(program_io,output_io,step_limit) 32 | # puts "output: ", output_io.string 33 | rescue AtlasError => e 34 | 35 | rescue => e 36 | STDERR.puts "failed, program was" 37 | STDERR.puts program 38 | raise e 39 | end 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/test_docs.rb: -------------------------------------------------------------------------------- 1 | require 'open3' 2 | runs = 0 3 | def failit(filename,line,prog,expected,output) 4 | puts "FAIL doc: " 5 | puts prog 6 | puts "Expecting:", expected 7 | puts "Found:", output 8 | puts "from: "+filename+" line: "+line.to_s 9 | exit(1) 10 | end 11 | 12 | (Dir['docs/*.md']<<"README.md").each{|doc| 13 | file = File.read(doc) 14 | tests = 0 15 | file.scan(/((?: .*\n+)+) ───+\n((:? .*\n+)+)/){|a| 16 | tests += 1 17 | line=$`.count($/)+1 18 | prog,expected=*a 19 | prog.gsub!(/^ /,'') 20 | expected.gsub!(/^ /,'') 21 | 22 | File.write("test/prog.atl",prog) 23 | 24 | truncate = expected =~ /\.\.\.$/ 25 | expected.gsub!(/\.\.\.$/,'') 26 | 27 | if truncate 28 | stdout, stderr, status = Open3.capture3("./atlas test/prog.atl | head -c #{truncate}") 29 | else 30 | stdout, stderr, status = Open3.capture3("./atlas test/prog.atl") 31 | end 32 | 33 | stderr=stderr.split("\e[31m").join 34 | stderr=stderr.split("\e[0m").join 35 | output = stdout + stderr 36 | output.gsub!(/ *$/,'') 37 | output.strip! 38 | expected.strip! 39 | 40 | if output != expected 41 | failit(doc,line,prog,expected,output) 42 | else 43 | runs += 1 44 | end 45 | } 46 | raise "probably have misformatted example" if tests != file.split(/───+/).size-1 47 | } 48 | 49 | puts "PASS %d doc tests" % runs 50 | -------------------------------------------------------------------------------- /error.rb: -------------------------------------------------------------------------------- 1 | def warn(msg, from=nil) 2 | STDERR.puts to_location(from) + " " + msg + " (Warning)" 3 | end 4 | 5 | def to_location(from) 6 | token = case from 7 | when Token 8 | from 9 | when AST 10 | from.token 11 | when IR 12 | from.from.token 13 | when NilClass 14 | nil 15 | else 16 | raise "unknown location type %p " % from 17 | end 18 | 19 | if token 20 | "%s:%s (%s)" % [token.line_no||"?", token.char_no||"?", token.str] 21 | else 22 | "?:?" 23 | end 24 | end 25 | 26 | class AtlasError < StandardError 27 | def initialize(message,from) 28 | @message = message 29 | @from = from 30 | end 31 | def message 32 | to_location(@from) + " " + @message + " (\e[31m#{class_name}\e[0m)" 33 | end 34 | def class_name 35 | self.class.to_s 36 | end 37 | end 38 | 39 | class DynamicError < AtlasError 40 | end 41 | 42 | class InfiniteLoopError < DynamicError 43 | attr_reader :source 44 | def initialize(message,source,token) 45 | @source = source # no longer needed, cleanup 46 | super(message,token) 47 | end 48 | end 49 | 50 | class StaticError < AtlasError 51 | end 52 | 53 | # Named this way to avoid conflicting with Ruby's TypeError 54 | class AtlasTypeError < StaticError 55 | def class_name 56 | "TypeError" 57 | end 58 | end 59 | 60 | class ParseError < StaticError 61 | end 62 | 63 | class LexError < StaticError 64 | def initialize(message) 65 | super(message,$from) 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /type.rb: -------------------------------------------------------------------------------- 1 | Inf = 2**61 # for max_pos_dim 2 | Type = Struct.new(:dim,:base_elem) # base is :int, :char, or :a 3 | # :a means unknown type, it could be any type with dim >= 0 4 | TypeWithVecLevel = Struct.new(:type,:vec_level) 5 | 6 | class Type 7 | def inspect 8 | #return "(%d %s)"%[dim,base_elem] if dim < 0 # ret nicely since could have negative type errors in circular inference that later becomes valid 9 | "["*dim + base_elem.to_s.capitalize + "]"*dim 10 | end 11 | def -(rhs) 12 | self+-rhs 13 | end 14 | def +(zip_level) 15 | Type.new(dim+zip_level, base_elem) 16 | end 17 | def max_pos_dim 18 | is_unknown ? Inf : dim 19 | end 20 | def string_dim # dim but string = 0 21 | dim + (is_char ? -1 : 0) 22 | end 23 | def is_char 24 | base_elem == :char 25 | end 26 | def is_unknown 27 | base_elem == :a 28 | end 29 | def can_base_be(rhs) # return true if self can be rhs 30 | return self.base_elem == rhs.base_elem 31 | end 32 | def default_value 33 | return [] if dim > 0 34 | return 32 if is_char 35 | return 0 if base_elem == :int 36 | raise DynamicError.new("access of the unknown type",nil) 37 | end 38 | end 39 | 40 | Int = Type.new(0,:int) 41 | Char = Type.new(0,:char) 42 | Str = Type.new(1,:char) 43 | Unknown = Type.new(0,:a) 44 | UnknownV0 = TypeWithVecLevel.new(Unknown,0) 45 | Empty = Unknown+1 46 | 47 | class TypeWithVecLevel 48 | def inspect 49 | #return "(%d %s)"%[vec_level,type.inspect] if vec_level < 0 # ret nicely since could have negative type errors in circular inference that later becomes valid 50 | "<"*vec_level + type.inspect + ">"*vec_level 51 | end 52 | end 53 | 54 | -------------------------------------------------------------------------------- /test/test_examples.rb: -------------------------------------------------------------------------------- 1 | require 'open3' 2 | runs = 0 3 | def failit(filename,test, reason) 4 | puts "FAIL example: " 5 | puts test 6 | puts reason 7 | puts "from: "+filename 8 | exit(1) 9 | end 10 | 11 | tests = Dir["test/examples/*.test"] 12 | section_regex = /^\[.*?\]\n/i 13 | tests.each{|test_filename| 14 | test = File.read(test_filename) 15 | sections = test.scan(section_regex) 16 | datum = test.split(section_regex)[1..-1] 17 | prog = nil 18 | args = "" 19 | expected_stderr = input = expected_stdout = "" 20 | sections.zip(datum){|section,data| 21 | case section.chomp[1...-1].downcase 22 | when "input" 23 | input = data.strip 24 | when "stdout" 25 | expected_stdout = data.strip 26 | when "stderr" 27 | expected_stderr = (data||"").strip 28 | when "prog" 29 | prog = data.strip 30 | when "args" 31 | args=data.strip 32 | else 33 | raise "unknown section %p" % section 34 | end 35 | } 36 | raise "invalid test #{test_filename}" if expected_stdout=="" && expected_stderr=="" 37 | 38 | File.write("test/input", input) 39 | File.write("test/prog.atl",prog) 40 | stdout, stderr, status = Open3.capture3("./atlas test/prog.atl < test/input") 41 | 42 | stdout.strip! 43 | stderr.strip! 44 | stderr=stderr.split("\e[31m").join 45 | stderr=stderr.split("\e[0m").join 46 | 47 | if !expected_stderr.empty? && !stderr[expected_stderr] || expected_stderr.empty? && !stderr.empty? 48 | failit(test_filename,test,"stderr was\n"+stderr) 49 | elsif stdout != expected_stdout 50 | failit(test_filename,test,"stdout was\n"+stdout) 51 | els 52 | else 53 | runs += 1 54 | end 55 | } 56 | puts "PASS %d example runs" % runs 57 | -------------------------------------------------------------------------------- /escape.rb: -------------------------------------------------------------------------------- 1 | def inspect_char(char) 2 | return "'\"" if char=='"'.ord # Don't escape this char (we would in a string) 3 | "'" + escape_str_char(char) 4 | end 5 | 6 | def escape_str_char(char) 7 | return "\\0" if char == "\0".ord 8 | return "\\n" if char == "\n".ord 9 | return "\\\\" if char == "\\".ord 10 | return "\\\"" if char == "\"".ord 11 | return "%c" % char if char >= " ".ord && char <= "~".ord # all ascii printables 12 | return "\\x0%s" % char.to_s(16) if char < 16 && char >= 0 13 | return "\\x%s" % char.to_s(16) if char < 256 && char >= 0 14 | begin 15 | return "%c" % char # most unicodes are printable, just print em 16 | rescue ArgumentError 17 | return "invalid char: %d" % char 18 | end 19 | end 20 | 21 | def parse_char(s) 22 | ans,offset=parse_str_char(s,0) 23 | raise "internal char error" if ans.size != 1 || offset != s.size 24 | ans[0].ord 25 | end 26 | 27 | def parse_str_char(s,i) # parse char in a string starting at position i 28 | if s[i]=="\\" 29 | if s.size <= i+1 30 | ["\\",i+1] 31 | elsif s[i+1] == "n" 32 | ["\n",i+2] 33 | elsif s[i+1] == "0" 34 | ["\0",i+2] 35 | elsif s[i+1] == "\\" 36 | ["\\",i+2] 37 | elsif s[i+1] == "\"" 38 | ["\"",i+2] 39 | elsif s[i+1,3] =~ /x[0-9a-fA-F][0-9a-fA-F]/ 40 | [s[i+2,2].to_i(16).chr,i+4] 41 | else 42 | ["\\",i+1] 43 | end 44 | else 45 | [s[i],i+1] 46 | end 47 | end 48 | 49 | def parse_str(s) 50 | i=0 51 | r="" 52 | while i 1 70 | # c=parse_char(s) 71 | # p [ci,c.ord,s] if c.ord != ci 72 | # } 73 | -------------------------------------------------------------------------------- /docs/io.md: -------------------------------------------------------------------------------- 1 | # IO 2 | 3 | As a pure language, Atlas has no operators for doing input or output, however it is fairly easy to accomplish these tasks. 4 | 5 | ## Output 6 | 7 | Since an Atlas program is essentially the same as typing it into a REPL. Each line is printed as it is defined. Assignments are not printed (unless it is the last line in a program). Before printing values they are first converted to strings by first joining with spaces, then newlines then double newlines depending on list depth. 8 | 9 | Infinite list will print until they error. 10 | 11 | ## Input 12 | 13 | The `$` token is stdin as a vector of strings, where each vector element is one line of input. Since Atlas is lazy you can accomplish interactive IO even though `$` is just a regular value. 14 | 15 | One note about lazy IO is that it will output anything as soon as it can, so if you wanted to write a program that asked for you name and then said hello to you: 16 | 17 | "Enter your name: " 18 | "Hello, " ($% head) 19 | 20 | Would print the hello before you typed anything because that will always precede whatever you input. 21 | 22 | One way around that is to trick laziness into thinking the hello depends on your input. A simple but hacky way is to reverse the result twice. 23 | 24 | "Enter your name: " 25 | "Hello, " ($% head) reverse reverse 26 | 27 | Simpler would be to just write 28 | 29 | "Enter your name: " 30 | "Hello, " $ 31 | 32 | That won't prematurely print hello, because you could input 0 lines and the implicit append operation is vectorized (since $ is a vector) and so it would say hello to however many names (lines) you input, possibly 0. 33 | 34 | ## Ints 35 | 36 | To get ints just use the `read` op (`&`) on `$`. 37 | 38 | ## Shorthand 39 | 40 | There is currently a shorthand for getting a column of ints from stdin and that is to use an unmatched `}`. This feels a bit hacky so I may remove it, but it is highly useful for the repetitive task of parsing input. 41 | -------------------------------------------------------------------------------- /lex.rb: -------------------------------------------------------------------------------- 1 | class Token \n and \t -> 8x" " and \r -> \n 19 | 20 | NumRx = /[0-9]+/ 21 | CharRx = /'(\\n|\\0|\\x[0-9a-fA-F][0-9a-fA-F]|.)/ 22 | StrRx = /"(\\.|[^"])*"?/ 23 | AtomRx = /#{CharRx}|#{NumRx}|#{StrRx}/ 24 | # if change, then change auto complete chars 25 | IdRx = /[a-z][a-zA-Z0-9_]*/ 26 | SymRx = /#{ModableSymbols.map{|c|Regexp.escape c}*'|'}/ 27 | OpRx = /#{IdRx}|#{ApplyRx}?#{SymRx}#{FlipRx}?|#{FlipRx}|#{ApplyRx}/ 28 | OtherRx = /#{UnmodableSymbols.map{|c|Regexp.escape c}*'|'}/ 29 | CommentRx = /--.*/ 30 | EmptyLineRx = /\n[ \t]*#{CommentRx}?/ 31 | IgnoreRx = /#{CommentRx}|#{EmptyLineRx}*\n[ \t]+| / 32 | 33 | def assertVar(token) 34 | raise ParseError.new "cannot set #{token.str}", token unless token.str =~ /^#{IdRx}$/ 35 | end 36 | 37 | def lex(code,line_no=1) # returns a list of lines which are a list of tokens 38 | tokens = [[]] 39 | char_no = 1 40 | code.scan(/#{AtomRx}|#{CommentRx}|#{OpRx}|#{OtherRx}|#{IgnoreRx}|./m) {|matches| 41 | $from=token=Token.new($&,char_no,line_no) 42 | line_no += $&.count("\n") 43 | if $&["\n"] 44 | char_no = $&.size-$&.rindex("\n") 45 | else 46 | char_no += $&.size 47 | end 48 | if token.str =~ /^#{IgnoreRx}$/ 49 | # pass 50 | elsif token.str == "\n" 51 | tokens[-1] << Token.new(:EOL,char_no,line_no) 52 | tokens << [] 53 | else 54 | tokens[-1] << token 55 | end 56 | } 57 | tokens[-1]< 1+2 4 | 1+2*3 -> 1+2*3 5 | 1+(2*3) -> 1+(2*3) 6 | 1~+2*3 -> 1~+2*3 7 | 1+2~*3 -> 1+2~*3 8 | 1+(2~)*3 -> 1+(2~)*3 9 | 1~~ -> 1~~ 10 | neg 2 -> ParseError 11 | 12 | -- Test implicit 13 | 1 2 -> 1 2 14 | 1 (2*3) -> 1 (2*3) 15 | (1*2) 3 -> 1*2 3 16 | 17 | 1 2 3 -> 1 2 3 18 | (1 2) 3 -> 1 2 3 19 | 1 (2 3) -> 1 (2 3) 20 | (1)(2)(3) -> 1 2 3 21 | (1)(2) (3) -> 1 2 3 22 | (1) (2)(3) -> 1 2 3 23 | (1 2) (3 4) -> 1 2 (3 4) 24 | (1 2) 3 (4 5) -> 1 2 3 (4 5) 25 | 26 | -- Test space doesnt do anything 27 | 1 + 2~ -> 1+2~ 28 | 1 + 2*3 -> 1+2*3 29 | 1 ~ -> 1~ 30 | 1+2 * 3+4 -> 1+2*3+4 31 | 1+2 3*4 -> 1+2 3*4 32 | (1+2 ) -> 1+2 33 | ( 1+2) -> 1+2 34 | 35 | -- test unbalanced parens 36 | 1+2)+3 -> 1+2+3 37 | 1+(2*3 -> 1+(2*3) 38 | 39 | -- Identifiers 40 | AA -> A A 41 | aA -> aA 42 | a_a -> a_a 43 | A_ A -> A_ A 44 | 45 | 1; head -> 1;[ 46 | 47 | -- Test apply 48 | 1+2@+3 -> 1+(2+3) 49 | 1+2@+3@+4 -> 1+(2+(3+4)) 50 | 1+2~@+3 -> 1+(2~+3) 51 | 1+2~\@+3 -> 1+(2~\+3) 52 | 1+2@~+3 -> 1+(2~)+3 53 | 1+2@~@+3 -> 1+(2~+3) 54 | 1@+2 -> 1+2 55 | 1@~ -> 1~ 56 | 1@ -> ParseError 57 | 1@1 -> 1@1 58 | 1@a -> 1@a 59 | EOF 60 | 61 | require "./repl.rb" 62 | 63 | class AST 64 | def ==(rhs) 65 | self.op == rhs.op && self.args.zip(rhs.args).all?{|s,r|s==r} 66 | end 67 | end 68 | 69 | class Op 70 | def ==(rhs) 71 | self.name == rhs.name 72 | end 73 | end 74 | 75 | start_line=2 76 | pass = 0 77 | name = $0.sub('test/test_','').sub(".rb","") 78 | tests.lines.each{|test| 79 | start_line += 1 80 | next if test.strip == "" || test =~ /^--/ 81 | i,o=test.split("-"+">") 82 | STDERR.puts "INVALID test #{test}" if !o 83 | o.strip! 84 | begin 85 | tokens,lines = lex(i) 86 | found = parse_line(tokens[0],[]) 87 | 88 | tokens,lines = lex(o) 89 | expected = parse_line(tokens[0],[]) 90 | rescue Exception 91 | found = $! 92 | end 93 | 94 | if o=~/Error/ ? found.class.to_s!=o : found != expected 95 | STDERR.puts "FAIL: #{name} test line #{start_line}" 96 | STDERR.puts i 97 | STDERR.puts "expected:" 98 | STDERR.puts expected 99 | STDERR.puts "found:" 100 | raise found if Exception === found 101 | STDERR.puts found 102 | exit(1) 103 | end 104 | 105 | pass += 1 106 | } 107 | 108 | puts "PASS #{pass} #{name} tests" 109 | -------------------------------------------------------------------------------- /repl.rb: -------------------------------------------------------------------------------- 1 | require "readline" 2 | Dir[__dir__+"/*.rb"].each{|f| require_relative f } 3 | HistFile = Dir.home + "/.atlas_history" 4 | 5 | def repl(input=nil,output=STDOUT,step_limit=Float::INFINITY) 6 | context={} 7 | 8 | stack=3.downto(0).map{|i| 9 | AST.new(create_op( 10 | name: "col#{i}", 11 | type: VecOf.new(VecOf.new(Int)), 12 | impl: int_col(i) 13 | ),[]) 14 | } 15 | 16 | line_no = 1 17 | 18 | if input 19 | input_fn = lambda { input.gets(nil) } 20 | elsif !ARGV.empty? 21 | input_fn = lambda { gets(nil) } 22 | else 23 | if File.exists? HistFile 24 | Readline::HISTORY.push *File.read(HistFile).split("\n") 25 | end 26 | input_fn = lambda { 27 | line = Readline.readline("\e[33m ᐳ \e[0m", true) 28 | File.open(HistFile,'a'){|f|f.puts line} unless !line || line.empty? 29 | line 30 | } 31 | Readline.completion_append_character = " " 32 | Readline.basic_word_break_characters = " \n\t1234567890~`!@\#$%^&*()_-+={[]}\\|:;'\",<.>/?" 33 | Readline.completion_proc = lambda{|s| 34 | all = context.keys + OpsList.filter(&:name).map(&:name) 35 | all << "ops" 36 | all.grep(/^#{Regexp.escape(s)}/) 37 | } 38 | end 39 | 40 | ast = nil 41 | file_args = !ARGV.empty? 42 | assignment = false 43 | stop = false 44 | until stop 45 | prev_context = context.dup 46 | line=input_fn.call 47 | begin 48 | if line==nil # eof 49 | stop = true # incase error is caught we still wish to stop 50 | if assignment # was last 51 | ir = to_ir(ast,context) 52 | printit(ir, output, step_limit) 53 | end 54 | break 55 | end 56 | token_lines,line_no=lex(line, line_no) 57 | token_lines.each{|tokens| # each line 58 | next if tokens[0].str == :EOL 59 | if tokens.size == 2 && (Ops1[tokens[0].str] || Ops2[tokens[0].str]) 60 | OpsList.filter{|o|[o.name, o.sym].include?(tokens[0].str)}.each(&:help) 61 | next 62 | elsif tokens.size == 2 && tokens[0].str == "ops" 63 | OpsList.each{|op|op.help(false)} 64 | next 65 | end 66 | 67 | if tokens.size > 2 && tokens[1].str=="=" && tokens[0].is_alpha 68 | assignment = true 69 | assertVar(tokens[0]) 70 | ast = parse_line(tokens[2..-1], stack) 71 | set(tokens[0], ast, context) 72 | else 73 | assignment = false 74 | ast = parse_line(tokens, stack) 75 | ir = to_ir(ast,context) 76 | printit(ir, output, step_limit) 77 | end 78 | } 79 | rescue AtlasError => e 80 | STDERR.puts e.message 81 | assignment = false 82 | context = prev_context 83 | rescue => e 84 | STDERR.puts "!!!This is an internal Altas error, please report the bug (via github issue or email name of this lang at golfscript.com)!!!\n\n" 85 | raise e 86 | end 87 | end # until 88 | end 89 | 90 | def printit(ir,output,step_limit) 91 | ir = IR.new(ToString, [ir]) 92 | infer(ir) 93 | run(ir,output,10000,step_limit) 94 | output.puts unless $last_was_newline 95 | end -------------------------------------------------------------------------------- /ir.rb: -------------------------------------------------------------------------------- 1 | $ir_node_count = 0 2 | class IR < Struct.new( 3 | :op, # these set during construction 4 | :args, 5 | :from, 6 | :type_with_vec_level, # this and rest calculted in infer 7 | :zip_level, 8 | :promise, 9 | :id, 10 | :in_q, 11 | :used_by, 12 | :rep_levels, 13 | :promote_levels, 14 | :last_error, 15 | :type_updates, # for detecting inf type 16 | :from_var) 17 | def initialize(*args) 18 | super(*args) 19 | self.id = $ir_node_count += 1 20 | end 21 | def type 22 | type_with_vec_level.type 23 | end 24 | def vec_level 25 | type_with_vec_level.vec_level 26 | end 27 | def type_error(msg) 28 | self.last_error ||= AtlasTypeError.new msg,self 29 | UnknownV0 30 | end 31 | end 32 | 33 | # creates an IR from an AST, replacing vars 34 | def to_ir(ast,context) 35 | ir = create_ir(ast,context) 36 | check_missing(ir,context,{}) 37 | ir = lookup_vars(ir,context,{}) 38 | ir 39 | end 40 | 41 | def set(t,ast,context) 42 | ir = context[t.str] = create_ir(ast, context) 43 | ir.from_var = t.str 44 | ir 45 | end 46 | 47 | def create_ir(node,context) # and register_vars 48 | if node.op.name == "let" 49 | raise ParseError.new("only identifiers may be set",node) if node.args[1].op.name != "var" 50 | set(node.args[1].token, node.args[0], context) 51 | else 52 | args=node.args.map{|arg|create_ir(arg,context)} 53 | op = node.op.dup 54 | if node.token && node.token.str =~ /#{FlipRx}$/ 55 | args.reverse! 56 | end 57 | IR.new(op,args,node) 58 | end 59 | end 60 | 61 | def check_missing(node,context,been) 62 | return node if been[node.id] 63 | been[node.id]=true 64 | if node.op.name == "var" 65 | name = node.from.token.str 66 | raise(ParseError.new("unset identifier %p" % name, node.from.token)) unless context.include? name 67 | #raise(ParseError.new("trivial self dependency is nonsensical", node.from.token)) if context[name] == node 68 | check_missing(context[name],context,been) 69 | else 70 | node.args.each{|arg| check_missing(arg, context,been) } 71 | end 72 | end 73 | 74 | def lookup_vars(node,context,been) 75 | return node if been[node.id] 76 | been[node.id]=true if node.op.name != "var" 77 | node.args.map!{|arg| lookup_vars(arg, context,been) } 78 | if node.op.name == "var" 79 | return IR.new(UnknownOp,[],node.from) if context[node.from.token.str] == node 80 | lookup_vars(context[node.from.token.str],context,been) 81 | else 82 | node 83 | end 84 | end 85 | 86 | def all_nodes(root) 87 | all = [] 88 | dfs(root){|node| all << node} 89 | all 90 | end 91 | 92 | module Status 93 | UNSEEN = 1 # in practice nil will be used 94 | PROCESSING = 2 95 | SEEN = 3 96 | end 97 | 98 | def dfs(root,cycle_fn:->_{},&post_fn) 99 | dfs_helper(root,[],cycle_fn,post_fn) 100 | end 101 | 102 | def dfs_helper(node,been,cycle_fn,post_fn) 103 | if been[node.id] == Status::SEEN 104 | 105 | elsif been[node.id] == Status::PROCESSING # cycle 106 | cycle_fn[node] 107 | else 108 | been[node.id] = Status::PROCESSING 109 | node.args.each{|arg| 110 | dfs_helper(arg,been,cycle_fn,post_fn) 111 | } 112 | post_fn[node] 113 | end 114 | been[node.id] = Status::SEEN 115 | return 116 | end 117 | 118 | -------------------------------------------------------------------------------- /docs/vectorization.md: -------------------------------------------------------------------------------- 1 | # Vectorization 2 | 3 | Any list can be turned into a vector by using the `.` operator. A vector is just a list, but it prefers to apply operands to its elements instead of the list as a whole. 4 | 5 | "abc","123" len 6 | "abc","123". len 7 | ────────────────────────────────── 8 | 2 9 | 3 3 10 | 11 | Automatic vectorization can occur to fit the type specification of an operator. For example, the type of `+` is `Int Int -> Int` and so if you give it a list for any of the arguments, their rank is too high and so it vectorizes said argument to bring it down. It is essentially performing a `map`. 12 | 13 | 1,2,3.+4 show 14 | 1,2,3+4 show 15 | ────────────────────────────────── 16 | <5,6,7> 17 | <5,6,7> 18 | 19 | Note that the type is a vector not a list, even though `1,2,3` is a list, it was first vectorized. You can convert a vector back to a list using `%`. But usually this isn't needed because unvectorization is also automatically done if an arguments rank is too low. 20 | 21 | 1,2,3+4% len 22 | 1,2,3+4 len 23 | ────────────────────────────────── 24 | 3 25 | 3 26 | 27 | If two vectors are present it pairs them off an performs the operation on each pair. It is essentially performing a `zipWith` 28 | 29 | (1,2,3) + (2,4,6) 30 | ────────────────────────────────── 31 | 3 6 9 32 | 33 | Longer lists are truncated to that of the smaller (this isn't the case for all vectorized languages, but useful in Atlas since we frequently use infinite lists). 34 | 35 | (1,2) + (2,4,6) 36 | ────────────────────────────────── 37 | 3 6 38 | 39 | Automatic vectorization can work on non scalar arguments as well. 40 | 41 | "1 2 3","4 5" read show 42 | ────────────────────────────────── 43 | <[1,2,3],[4,5]> 44 | 45 | Read expects a string, but was given a list of strings so it vectorizes. Read returns a list of ints always, so that part is just normal operation. 46 | 47 | It can even work on more complicated types like the type of append (`[a] [a] -> [a]`). 48 | 49 | "abc","xyz" append "123" show 50 | ────────────────────────────────── 51 | <"abc123","xyz123"> 52 | 53 | Automatic Vectorization can only lower rank, sometimes it needs to be raised. For example transpose works on 2D lists, but if you give it a 1D list it needs to become a 2D list first, by just making it a list with a single element (the original list). I call this promotion. 54 | 55 | "123" \ show 56 | ────────────────────────────────── 57 | ["1","2","3"] 58 | 59 | Automatic promotion and vectorization can both be done implicitly together. For example: 60 | 61 | 'a take (1,0) show 62 | ────────────────────────────────── 63 | <"a",""> 64 | 65 | The `(1,0)` is vectorized and the `'a` is promoted to a string. 66 | 67 | Unvectorization is preferred to promotion. That is why the earlier example `1,2,3+4 len` returned `3` instead of `[1,1,1]`. 68 | 69 | There is one exception to these rules which is for `,` and this is to enable intuitive list construction from data. This is how `"abc","123","xyz"` creates a list of strings. Without preferring promotion over vectorization of the first arg, `,` would need to be type `a a -> [a]` to get the first use to work and type `[a] a -> [a]` to get the second op to work as well. `,` just prefers to promote once rather than automatically vectorize the first arg, you can still vectorize that arg, you will just need to do so explicitly. 70 | -------------------------------------------------------------------------------- /spec.rb: -------------------------------------------------------------------------------- 1 | # general type vars 2 | A = :a 3 | B = :b 4 | 5 | # type var of base elem char 6 | Achar = :a_char 7 | 8 | # type var of base elem int 9 | Aint = :a_int 10 | 11 | # type spec ================================ 12 | # { from => to } or just "to" if no args 13 | # fyi you can't use [type] unless you mean list of type (doesn't mean 1) 14 | # to can be a list for multiple rets or a type 15 | # from can be a list for multiple args or a conditional type 16 | # conditional type can be a type or a type qualifier of a type 17 | # type can be an actual type or a list of a type (recursively) or a type var e.g. :a 18 | class FnType < Struct.new(:specs,:ret,:orig_key,:orig_val) 19 | def inspect 20 | specs.map(&:inspect)*" "+" -> "+parse_raw_arg_spec(ret).inspect 21 | end 22 | end 23 | 24 | class VecOf < Struct.new(:of) 25 | end 26 | 27 | def create_specs(raw_spec) 28 | case raw_spec 29 | when Hash 30 | raw_spec.map{|raw_arg,ret| 31 | specs = (x=case raw_arg 32 | when Array 33 | if raw_arg.size == 1 34 | [raw_arg] 35 | else 36 | raw_arg 37 | end 38 | else 39 | [raw_arg] 40 | end).map{|a_raw_arg| parse_raw_arg_spec(a_raw_arg) } 41 | FnType.new(specs,ret,raw_arg,ret) 42 | } 43 | when Type, Array, VecOf, Symbol 44 | [FnType.new([],raw_spec,[],raw_spec)] 45 | else 46 | raise "unknown fn type format" 47 | end 48 | end 49 | 50 | def parse_raw_arg_spec(raw,list_nest_depth=0) 51 | case raw 52 | when Symbol 53 | VarTypeSpec.new(raw,list_nest_depth) 54 | when Array 55 | raise if raw.size != 1 56 | parse_raw_arg_spec(raw[0],list_nest_depth+1) 57 | when VecOf 58 | r=parse_raw_arg_spec(raw.of) 59 | r.vec_of=true 60 | r 61 | when Type 62 | ExactTypeSpec.new(Type.new(raw.dim+list_nest_depth, raw.base_elem)) 63 | else 64 | p raw 65 | error 66 | end 67 | end 68 | 69 | class ExactTypeSpec 70 | attr_reader :type 71 | attr_accessor :vec_of 72 | def initialize(rtype) 73 | @type = rtype 74 | end 75 | def check_base_elem(uses,t) 76 | t.can_base_be(@type) 77 | end 78 | def inspect 79 | (vec_of ? "<" : "")+type.inspect+(vec_of ? ">" : "") 80 | end 81 | end 82 | 83 | class VarTypeSpec 84 | attr_reader :var_name 85 | attr_reader :extra_dims 86 | attr_accessor :vec_of 87 | def initialize(var_sym, extra_dims) # e.g. [[a]] is .new(:a, 2) 88 | @var_name,@var_constraint = name_and_constraint(var_sym) 89 | @extra_dims = extra_dims 90 | @vec_of = false 91 | end 92 | def check_base_elem(uses,type) 93 | if @var_constraint 94 | @var_constraint == type.base_elem.to_s 95 | else 96 | type.base_elem == Unknown.base_elem || (uses[var_name]||=type.base_elem) == type.base_elem 97 | end 98 | end 99 | def inspect 100 | constraint = @var_constraint ? " (#{@var_constraint})" : "" 101 | (vec_of ? "<" : "")+"["*extra_dims+var_name.to_s+constraint+"]"*extra_dims+(vec_of ? ">" : "") 102 | end 103 | end 104 | 105 | def name_and_constraint(var_sym) 106 | s = var_sym.to_s.split("_") 107 | [s[0].to_sym, s[1]] 108 | end 109 | 110 | def spec_to_type(spec, vars) 111 | case spec 112 | when Type 113 | TypeWithVecLevel.new(spec,0) 114 | when Array 115 | raise "cannot return multiple values for now" if spec.size != 1 116 | TypeWithVecLevel.new(spec_to_type(spec[0], vars).type + 1, 0) 117 | when Symbol 118 | name,constraint=name_and_constraint(spec) 119 | t=TypeWithVecLevel.new(vars[name],0) 120 | t.type.base_elem = constraint.to_sym if constraint 121 | t 122 | when VecOf 123 | t=spec_to_type(spec.of, vars) 124 | TypeWithVecLevel.new(t.type, t.vec_level+1) 125 | else 126 | unknown 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /parse.rb: -------------------------------------------------------------------------------- 1 | AST = Struct.new(:op,:args,:token) 2 | 3 | def parse_line(tokens, stack) 4 | ast = get_expr(tokens,:EOL) 5 | handle_push_pops(ast, stack) 6 | end 7 | 8 | DelimiterPriority = {:EOL => 0, ')' => 1} 9 | LBrackets = {"(" => ")"} 10 | 11 | def get_expr(tokens,delimiter) 12 | lastop = implicit_var = nil 13 | nodes = [] 14 | loop { 15 | atom,t = get_atom(tokens) 16 | if atom 17 | if lastop #binary op 18 | nodes << implicit_var = new_var if nodes.empty? 19 | if lastop.str == ApplyModifier && atom.op.name == "var" 20 | # would register as a modifier to implicit op, override this 21 | nodes << AST.new(Ops2[ApplyModifier], [], lastop) << atom 22 | else 23 | nodes << make_op2(lastop) << atom 24 | end 25 | elsif nodes.empty? #first atom 26 | nodes << atom 27 | else # implict op 28 | nodes << AST.new(Ops2[" "],[],t) << atom 29 | end 30 | lastop = nil 31 | else # not an atom 32 | if lastop 33 | nodes << implicit_var = new_var if nodes.empty? 34 | if lastop.str =~ /(.*)(#{FlipRx})$/ 35 | if !$1.empty? # lexer falsely thought it was an op modifier, split it into 2 tokens 36 | z=lastop.dup 37 | z.str = $1 38 | nodes << make_op1(z) 39 | lastop.char_no += $1.size 40 | lastop.str = $2 41 | end 42 | nodes << AST.new(Ops1[FlipModifier], [], lastop) 43 | else 44 | nodes << make_op1(lastop) 45 | end 46 | end 47 | 48 | if DelimiterPriority[t.str] 49 | nodes << AST.new(EmptyOp,[],t) if nodes.empty? 50 | if implicit_var 51 | nodes << AST.new(Ops2['let'], [], t) << implicit_var 52 | implicit_var = nil 53 | end 54 | if t.str != delimiter 55 | if DelimiterPriority[t.str] >= DelimiterPriority[delimiter] 56 | # e.g. encountered ) but no ( to match, do nothing except process implicit_var 57 | next 58 | else # e.g. token is eof, expecting ) 59 | # return without consuming token 60 | tokens.unshift t 61 | end 62 | end 63 | 64 | break 65 | end 66 | lastop = t 67 | end 68 | } 69 | 70 | ops=[] 71 | atoms=[] 72 | until nodes.empty? 73 | o = nodes.pop 74 | if o.token.str[/^#{ApplyRx}/] && o.op.name != "let" && o.args.size < o.op.narg 75 | x = nodes[-1] 76 | while nodes[-1].args.size 0 141 | if ast.op.name == "push" 142 | ast = AST.new(Ops2["let"], [ast.args[0], new_var], ast.token) 143 | stack.push ast 144 | elsif ast.op.name == "pop" 145 | raise ParseError.new("pop on empty stack", ast) if stack.empty? 146 | ast = stack.pop 147 | end 148 | ast.args[1] = handle_push_pops(ast.args[1], stack) if ast.args.size > 1 149 | 150 | ast 151 | end 152 | 153 | $new_vars = 0 154 | def new_var 155 | AST.new(Var,[],Token.new("_T#{$new_vars+=1}")) 156 | end 157 | -------------------------------------------------------------------------------- /docs/quickref.md: -------------------------------------------------------------------------------- 1 | # todo auto generate 2 | 3 | for now this is just what you get when you type `ops` into the repl (organized slightly). 4 | 5 | LIST OPS ##################### 6 | head [ 7 | [a] → a 8 | "abc"[ → 'a 9 | 10 | last ] 11 | [a] → a 12 | "abc"] → 'c 13 | 14 | tail > 15 | [a] → [a] 16 | "abc"> → "bc" 17 | 18 | init < 19 | [a] → [a] 20 | "abc"< → "ab" 21 | 22 | len # 23 | [a] → Int 24 | "asdf"# → 4 25 | 26 | single ; 27 | a → [a] 28 | 2; → [2] 29 | 30 | take [ 31 | [a] Int → [a] 32 | "abcd"[3 → "abc" 33 | 34 | drop ] 35 | [a] Int → [a] 36 | "abcd"]3 → "d" 37 | 38 | count = 39 | [a] → [Int] 40 | "abcaab" count → [0,0,0,1,2,1] 41 | 42 | filter ? 43 | [a] [b] → [a] 44 | "abcd" ? (0,1,1,0) → "bc" 45 | 46 | sort ! 47 | [a] → [a] 48 | "atlas" ! → "aalst" 49 | 50 | sortBy ! 51 | [a] [b] → [a] 52 | "abc" ! (3,1,2) → "bca" 53 | 54 | chunkWhile ~ 55 | chunk while second arg is truthy, resulting groups are of the form [truthy, falsey] 56 | [a] [b] → [[[a]]] 57 | "abcd" ~ "11 1" → [["ab","c"],["d",""]] 58 | 59 | concat _ 60 | [[a]] → [a] 61 | "abc","123"_ → "abc123" 62 | 63 | append _ 64 | [a] [a] -> [a] (coerces) 65 | "abc"_"123" → "abc123" 66 | 67 | cons ` 68 | [a] a -> a (coerces) 69 | "abc"`'d → "dabc" 70 | 71 | snoc , 72 | rear cons, promote of first arg is allowed for easy list construction 73 | [a] a -> a (coerces) 74 | 1,2,3 → [1,2,3] 75 | 76 | transpose \ 77 | [[a]] → [[a]] 78 | "abc","123"\ → ["a1","b2","c3"] 79 | 80 | reverse / 81 | [a] → [a] 82 | "abc" reverse → "cba" 83 | 84 | 85 | 86 | MATH OPS ##################### 87 | add + 88 | Int Int → Int 89 | Int Char → Char 90 | Char Int → Char 91 | 1+2 → 3 92 | 93 | sub - 94 | Int Int → Int 95 | Char Int → Char 96 | Char Char → Int 97 | 5-3 → 2 98 | 99 | mult * 100 | Int Int → Int 101 | 2*3 → 6 102 | 103 | pow ^ 104 | Int Int → Int 105 | 2^3 → 8 106 | 107 | div / 108 | Int Int → Int 109 | 7/3 → 2 110 | 111 | mod % 112 | Int Int → Int 113 | 7%3 → 1 114 | 115 | neg - 116 | Int → Int 117 | 2- → -2 118 | 119 | abs | 120 | Int → Int 121 | 2-,3| → <2,3> 122 | 123 | STRING OPS ##################### 124 | join * 125 | [Str] Str → Str 126 | [Int] Str → Str 127 | "hi","yo"*" " → "hi yo" 128 | 129 | split / 130 | Str Str → [Str] 131 | "hi, yo"/", " → ["hi","yo"] 132 | 133 | replicate ^ 134 | Str Int → Str 135 | "ab"^3 → "ababab" 136 | 137 | LOGICAL OPS ##################### 138 | not ~ 139 | a → Int 140 | 2,0.~ → <0,1> 141 | 142 | eq = 143 | a a → [a] 144 | 3=3 → [3] 145 | 146 | lessThan < 147 | a a → [a] 148 | 4<5 → [5] 149 | 150 | greaterThan > 151 | a a → [a] 152 | 5>4 → [4] 153 | 154 | and & 155 | a b → b 156 | 1&2,(0&2) → [2,0] 157 | 158 | or | 159 | a a -> a (coerces) 160 | 1|2,(0|2) → [1,2] 161 | 162 | IO OPS ##################### 163 | read & 164 | Str → [Int] 165 | "1 2 -3"& → [1,2,-3] 166 | 167 | input $ 168 | all lines of stdin 169 | → [Str] 170 | 171 | str ` 172 | Int → Str 173 | 12` → "12" 174 | 175 | DEBUG ##################### 176 | show p 177 | a → Str 178 | 12p → "12" 179 | no_zip=true 180 | 181 | type 182 | a → Str 183 | 1 type → "Int" 184 | no_zip=true 185 | 186 | version 187 | → Str 188 | 189 | reductions 190 | operation count so far 191 | → Int 192 | 193 | 194 | VECTOR ##################### 195 | repeat , 196 | a → 197 | 2, → <2,2,2,2,2... 198 | 199 | range : 200 | Int Int → 201 | Char Char → 202 | 3:7 → <3,4,5,6> 203 | 204 | from : 205 | Int → 206 | Char → 207 | 3: → <3,4,5,6,7,8... 208 | 209 | unvec % 210 | → [a] 211 | 1,2+3% → [4,5] 212 | 213 | vectorize . 214 | [a] → 215 | 1,2,3. → <1,2,3> 216 | 217 | META ##################### 218 | let @ 219 | save to a variable without consuming it 220 | a a → [a] 221 | 5@a+a → 10 222 | 223 | push { 224 | duplicate arg onto a lexical stack 225 | a → a 226 | 5{,1,},2 → [5,1,5,2] 227 | 228 | pop } 229 | pop last push arg from a lexical stack 230 | → a 231 | 5{,1,},2 → [5,1,5,2] 232 | 233 | flip \ 234 | reverse order of previous op's args 235 | (a→b→c) → (b→a→c) 236 | 2-\5 → 3 237 | 238 | apply @ 239 | increase precedence, apply next op before previous op 240 | (a→b→c) → (a→b→c) 241 | (a→b) → (a→b) 242 | 2*3@+4 → 14 243 | 244 | -------------------------------------------------------------------------------- /test/examples/large_prog.test: -------------------------------------------------------------------------------- 1 | [prog] 2 | 1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1 3 | 1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(1 4 | [stdout] 5 | 1000 6 | 1000 -------------------------------------------------------------------------------- /docs/types.md: -------------------------------------------------------------------------------- 1 | # Types 2 | 3 | There are 5 types in Atlas. 4 | - Integers (arbitrary precision). 5 | - Chars which are just integers that may have a different set of operations allowed on them including how they are displayed. Construct them using a single leading `'`. 6 | - Lists, which may also be of other lists. A list of a list of integers is what I call a 2D list AKA a rank 2 list. This is not a matrix, each sublist may have a different length. 7 | - Vectors, which are just list that prefer to apply operands to their elements instead of the list as a whole. See the [vectorization](vectorization.md) section for information about how automatic vectorization rules. 8 | - A, the unknown type. The empty list `()` is a list of this type. 9 | 10 | Strings are just lists of characters. 11 | 12 | 123 -- This is an integer 13 | 'x -- This is a char 14 | "abc" -- This is a string, aka list of chars 15 | 1,2,3 -- This is a list of integers constructed via the snoc (cons on end) operator. 16 | () show -- This is an empty list pretty printed 17 | ────────────────────────────────── 18 | 123 19 | x 20 | abc 21 | 1 2 3 22 | [] 23 | 24 | Some escapes are possible in chars/strings: 25 | 26 | `\0` `\n` `\"` `\\` `\x[0-9a-f][0-9a-f]` 27 | 28 | You may also use any unicode character, the Atlas files are assumed to be UTF-8 encoded. 29 | 30 | Integers are truthy if >0, chars if non whitespace, lists if non empty. This is only used by operators like `and`, `or`, `filter`, `not`, etc. 31 | 32 | Atlas is statically typed. Inference works in a top down fashion. You shouldn't have to think about it or even notice it since you never need to specify type. The only thing it prevents is heterogeneous lists, but there are major advantages to homogeneous lists when it comes to implicit vectorization. Homogeneous lists could be done dynamically, but static typing is useful for circular programs it allows for selecting of op behavior and vectorization before evaluating the args. 33 | 34 | I'm very happy with how inference turned out. You will never need to specify a type or get an error message about an ambiguous type, etc. It should feel as easy as dynamic typing but with better error messages and more consistent op overloading / coercion. 35 | 36 | Unknown needs to be it's own type (as opposed to solving a type variable like Haskell) in order for top down type inference to work (which is what Atlas uses). It becomes the smallest rank type it can when used in ops with type constraints like `append`. You may also see this type displayed as the arg type in invalid circular program's error messages, this is because the type of all values in a circular definition are considered unknown until they can be inferred. If they are never inferred their type remains unknown. It is not possible to construct program that would do anything except infinite loop or error if a value of type unknown is used, so this is not a limitation of the inference. 37 | 38 | Doing type inference on circular programs that can have implicit vectorization was a very tricky problem to solve - although the code to implement it is very simple, it works by treating the types as a lattice. There is no need to understand how it works, but I will explain it, mostly so that I can read this when I forget. 39 | 40 | For non circular programs, inference is trivial and works in a top down fashion, but for circular programs there is nowhere to start. It starts anywhere in the circle with a guess that its arg types are type unknown and recomputes the type of the resulting op, if the result is different than before it then recomputes types that depended on that recursively. 41 | 42 | So long as there is no oscillation between types this process will eventually terminate or try to construct an infinite type (which is invalid). This is what I meant by it being a lattice. So how do we know no oscillation is possible? 43 | 44 | - Base elements could actually oscillate, consider the code `'b-a`, it will return an int if `a` is a char, but a char if `a` is an int. However this doesn't matter because any circular program at the scalar level would definitely be an infinite loop. 45 | - List depth can only increase or stay the same if their argument's list depth increase. 46 | - Vector depth can only increase or stay the same if their argument's vector depth increase. And decreasing the vector depth cannot decrease list depth. 47 | 48 | This last point is important since decreasing list depth can increase vector depth. Consider the code: 49 | 50 | "abcd" = 'c len show 51 | ────────────────────────────────── 52 | <0,0,1,0> 53 | 54 | If we increase the right arg to a string: 55 | 56 | "abcd" = "c" len show 57 | ────────────────────────────────── 58 | 0 59 | 60 | The result has the same list depth (0), but a lower vector depth. But since there is no way to convert this vector depth decrease back into a list depth decrease, it is safe. Any op that would unvectorize would also just promote if the vector depth was too low. 61 | 62 | One could easily violate these lattice properties when designing op behavior and auto vectorization rules. For example suppose you created an op that removed all vectorization (2d vector would return a 2d list for example). This would violate a rule. The current `unvec` op always removes exactly 1 layer. It might be worthwhile to even create tests that none of the ops can violate the rules. If you ever do encounter a program that oscillates it will give you a special error asking you to report the bug, please do so! 63 | -------------------------------------------------------------------------------- /docs/syntax.md: -------------------------------------------------------------------------------- 1 | # Syntax 2 | 3 | See the [readme](../README.md) for syntax basics. 4 | 5 | Atlas uses infix syntax for all operations. It's precedence is left to right. Atlas tries to compromise between simplicity, familiarity, and conciseness. Sticking to math like precedence would be more familiar, but is actually quite complicated and inconsistent. Similar reasoning as APL is also used here in that there are too many ops to keep track of the precedence of each. It's actually very easy to get used to the lack of op specific precedence. 6 | 7 | All ops have a symbol version and named version. This is so that code can be written in a legible way (even to newcomers) but you can also make it short if you want to. I have no idea why APL and variants don't do this. Even the named versions are still written infix. For example: 8 | 9 | 4 add 5 10 | ────────────────────────────────── 11 | 9 12 | 13 | You can think of it like an OO languages `a.add(5)` if that helps. 14 | 15 | Atlas is actually a just a REPL calculator and so if you have multiple lines it is just treated as multiple expressions that are all printed. 16 | 17 | You can still have multiline expressions so long as you indent the extra lines. 18 | 19 | 1*2 20 | +4 21 | ────────────────────────────────── 22 | 6 23 | 24 | Single line comments are done with `--`. I recommend setting your syntax highlighting to be Haskell for `.atl` files for an easy keyboard shortcut. If you wanted to negate then subtract, just add then negate instead. 25 | 26 | 1+1--this is ignored 27 | ────────────────────────────────── 28 | 2 29 | 30 | 31 | Since all ops are overloaded as both unary and binary operators if there are multiple ops in a row, the last is the binary operator and the rest are unary (it is the only way that makes sense). 32 | 33 | 3-|+1 -- that is negate then abs value 34 | ────────────────────────────────── 35 | 4 36 | 37 | The `+` was a binary op, and the `-` and `|` are unary. 38 | 39 | Two expressions in a row without an explicit operation do an implicit op. For numbers this multiplies, and for strings it appends. You don't necessarily need a space to use this. This implicit operation is still left to right and equal precedence to other operations. 40 | 41 | 1+2 3*4 42 | -- Is parsed as: 43 | ((1+2) 3)*4 44 | -- And does an implict multiplication 45 | ────────────────────────────────── 46 | 36 47 | 36 48 | 49 | If you want to use the implicit op following a unary op, it would look like you were trying to just do a binary op instead. To overcome this just be explicit and use `*` or `_`. 50 | 51 | 2-*3 52 | -- or you could use parenthesis 53 | (2-)3 54 | ────────────────────────────────── 55 | -6 56 | -6 57 | 58 | In addition to assignment, `=` is also used to test equality, it is only used as assignment if first on a line and the left hand side is an identifier. 59 | 60 | `()` is the empty list. 61 | 62 | () show 63 | ────────────────────────────────── 64 | [] 65 | 66 | Identifiers must start with a letter but then can have numbers or underscores. 67 | 68 | Parens do *not* need to be matched 69 | 70 | )p 71 | 2*(3+4 72 | ────────────────────────────────── 73 | [] 74 | 14 75 | 76 | `@` is an op modifier that flips the argument order of the next op. It can be used in a nested manner. 77 | 78 | 2*1@+1@+1 79 | 2*(1+(1+1)) 80 | ────────────────────────────────── 81 | 6 82 | 6 83 | 84 | It can also be used on unary ops. It will be done implicitly on unary ops if used on a binary op right after it. 85 | 86 | 2*1-@+1 87 | 2*1@-@+1 88 | 2*((1-)+1) 89 | ────────────────────────────────── 90 | 0 91 | 0 92 | 0 93 | 94 | `@` is also an assignment that does not consume the value (if there is an identifier on the right). 95 | 96 | 1+2@a*a 97 | ────────────────────────────────── 98 | 9 99 | 100 | `\` is also a modifier that flips the op order of the previous op. 101 | 102 | 5-\8 103 | ────────────────────────────────── 104 | 3 105 | 106 | The reason for it being after the op is so that it can also be used as a regular unary op as well (`transpose`). 107 | 108 | Both of these modifiers can be used on the implicit op. 109 | 110 | "hi"\"there" 111 | 2+3@5 112 | ────────────────────────────────── 113 | therehi 114 | 17 115 | 116 | `{` and `}` may seem special syntactically, but they are not. `{` is a unary op that "pushes" on to a stack (that only exists at parse time) so that the next `}` (which is just an atom) can access the same value. It is equivalent to using assignment or `@`, but shorter when a value is reused only once. It has a nice appearance in that visually matching brackets will tell you which one corresponds to which. 117 | 118 | 5*2{*} 119 | ────────────────────────────────── 120 | 100 121 | 122 | The curly brackets are nice for avoiding normal variable assignments, but cannot help you write a circular program since they always copy the left value. To do that just use parenthesis with an implicit value. Instead of writing `a cons 1@a` we could just write: 123 | 124 | (cons 1) 125 | ────────────────────────────────── 126 | 1 1 1 1 1 1... 127 | 128 | The parenthesis are not actually needed in this case since there is nothing else, I suspect people may use this feature accidentally and so I may require parenthesis to always be used in the future. -------------------------------------------------------------------------------- /infer.rb: -------------------------------------------------------------------------------- 1 | def infer(root) 2 | all = all_nodes(root) 3 | q=[] 4 | # these are topologically sorted from post traversal dfs which gives a favorable order to start inference from 5 | all.each{|node| 6 | node.used_by = []; 7 | if node.type_with_vec_level == nil 8 | node.type_with_vec_level = UnknownV0 9 | node.in_q = true 10 | q << node 11 | end 12 | } 13 | all.each{|node|node.args.each{|arg| arg.used_by << node} } 14 | 15 | q.each{|node| # this uses q as a queue 16 | node.in_q = false 17 | prev_type = node.type_with_vec_level 18 | calc_type(node) 19 | if node.type_with_vec_level != prev_type && !node.last_error 20 | node.type_updates = (node.type_updates || 0) + 1 21 | if node.type_updates > 100 22 | if node.type.dim < 20 && node.vec_level < 20 23 | raise "congratulations you have found a program that does not find a fixed point for its type, please report this discovery - I am not sure if it possible and would like to know" 24 | end 25 | raise AtlasTypeError.new "cannot construct the infinite type" ,node 26 | end 27 | 28 | node.used_by.each{|dep| 29 | if !dep.in_q 30 | dep.in_q = true 31 | q << dep 32 | end 33 | } 34 | end 35 | } 36 | 37 | errors = [] 38 | dfs(root) { |node| 39 | if node.last_error 40 | errors << node.last_error if node.args.all?{|arg| arg.type_with_vec_level != nil } 41 | node.type_with_vec_level = nil 42 | end 43 | } 44 | errors[0...-1].each{|error| STDERR.puts error.message } 45 | raise errors[-1] if !errors.empty? 46 | end 47 | 48 | def calc_type(node) 49 | node.last_error = nil 50 | fn_types = node.op.type.select{|fn_type| 51 | check_base_elem_constraints(fn_type.specs, node.args.map(&:type)) 52 | } 53 | 54 | return node.type_error "op is #{fn_types.size==0?'not definied':'ambiguous'} for arg types: " + node.args.map{|arg|arg.type_with_vec_level.inspect}*',' if fn_types.size != 1 55 | 56 | node.type_with_vec_level = possible_types(node,fn_types[0]) 57 | end 58 | 59 | def possible_types(node, fn_type) 60 | if !node.op.no_zip 61 | arg_types = node.args.map(&:type) 62 | vec_levels = node.args.map(&:vec_level) 63 | else 64 | arg_types = node.args.map{|a|a.type+a.vec_level} 65 | vec_levels = node.args.map{|a|0} 66 | end 67 | 68 | vec_levels = vec_levels.zip(fn_type.specs).map{|vec_level,spec| 69 | if spec.vec_of 70 | return node.type_error "vec level is 0, cannot lower" if vec_level == 0 71 | vec_level - 1 72 | else 73 | vec_level 74 | end 75 | } 76 | 77 | nargs = arg_types.size 78 | vars = solve_type_vars(arg_types, fn_type.specs) 79 | deficits = rank_deficits(arg_types, fn_type.specs, vars) 80 | t = spec_to_type(fn_type.ret, vars) 81 | rep_levels = [0]*nargs 82 | promote_levels = [0]*nargs 83 | 84 | if node.op.name == "snoc" && deficits[1]<0 #&& arg_types[0] == arg_types[1] 85 | deficits[1] += 1 86 | promote_levels[0] += 1 87 | t = TypeWithVecLevel.new(t.type+1,t.vec_level) 88 | end 89 | 90 | nargs.times{|i| 91 | if deficits[i]>0 92 | if deficits[i] > vec_levels[i] || node.args[i].op.name == "vectorize" 93 | if node.op.no_promote 94 | return node.type_error "rank too low for arg #{i+1}" 95 | elsif node.args[i].op.name == "vectorize" 96 | promote_levels[i] += deficits[i] 97 | deficits[i] = 0 98 | else 99 | promote_levels[i] += deficits[i] - vec_levels[i] 100 | deficits[i] = vec_levels[i] 101 | end 102 | end 103 | elsif deficits[i] < 0 104 | rep_levels[i] -= deficits[i] 105 | end 106 | vec_levels[i] -= deficits[i] 107 | } 108 | zip_level = vec_levels.max || 0 109 | zip_level = 0 if node.op.no_zip 110 | nargs.times{|i| 111 | rep_levels[i] += zip_level - vec_levels[i] - rep_levels[i] 112 | return node.type_error "rank too high for arg #{i+1}" if rep_levels[i] > zip_level 113 | } 114 | node.zip_level = zip_level 115 | node.rep_levels = rep_levels 116 | node.promote_levels = promote_levels 117 | 118 | t.vec_level += zip_level 119 | t 120 | end 121 | 122 | def solve_type_vars(arg_types, specs) 123 | vars = {} # todo separate hash for ret and uses? 124 | 125 | arg_types.zip(specs) { |arg,spec| 126 | case spec 127 | when VarTypeSpec 128 | (vars[spec.var_name]||=[]) << arg - spec.extra_dims 129 | when ExactTypeSpec 130 | else 131 | error 132 | end 133 | } 134 | 135 | vars.each{|name,uses| 136 | max_min_dim = uses.reject(&:is_unknown).map(&:dim).min 137 | base_elems = uses.map(&:base_elem).uniq 138 | base_elem = if base_elems == [Unknown.base_elem] 139 | max_min_dim = uses.map(&:dim).max 140 | Unknown.base_elem 141 | else 142 | base_elems -= [Unknown.base_elem] 143 | base_elems[0] 144 | end 145 | 146 | vars[name] = Type.new([max_min_dim,0].max, base_elem) 147 | } 148 | vars 149 | end 150 | 151 | def rank_deficits(arg_types, specs, vars) 152 | arg_types.zip(specs).map{|arg,spec| 153 | spec_dim = case spec 154 | when VarTypeSpec 155 | vars[spec.var_name].max_pos_dim + spec.extra_dims 156 | when ExactTypeSpec 157 | spec.type.dim 158 | else 159 | error 160 | end 161 | if arg.is_unknown 162 | [spec_dim - arg.dim, 0].min 163 | else 164 | spec_dim - arg.dim 165 | end 166 | } 167 | end 168 | 169 | def check_base_elem_constraints(specs, arg_types) 170 | uses={} 171 | arg_types.zip(specs).all?{|type,spec| 172 | spec.check_base_elem(uses,type) 173 | } 174 | end 175 | -------------------------------------------------------------------------------- /test/behavior_tests.atl: -------------------------------------------------------------------------------- 1 | -- test ints 2 | 4 -> 4 3 | 45 -> 45 4 | 5 | -- test string and escapes 6 | "" -> "" 7 | "hi" -> "hi" 8 | "--" -> "--" 9 | "\n\\".; -> <"\n","\\"> 10 | "\x02\x20\xaa".; -> <"\x02"," ","\xaa"> 11 | 12 | -- test utf8 encoding chars 13 | "├───╯".; -> <"├","─","─","─","╯"> 14 | 15 | -- test char and escapes 16 | 'a -> 'a 17 | '\n -> '\n 18 | '\0 -> '\0 19 | '\ -> '\\ 20 | '" -> '" 21 | '' -> '' 22 | '-> LexError 23 | '\xff-'\0 -> 255 24 | '\xff -> '\xff 25 | '\0+5 -> '\x05 26 | 27 | -- Test implicit op 28 | 1+1 3 -> 6 29 | (1+1)3 -> 6 30 | 1,2"b" -> <"1b","2b"> 31 | "abc"+0"123" -> "abc123" 32 | ("a","c")"d" -> <"ad","cd"> 33 | 34 | -- Test replication in auto vectorization 35 | "abc","def","xyz"[(1,2,3) -> <["abc"],["abc","def"],["abc","def","xyz"]> 36 | 37 | -- Test auto promotion 38 | 5\ -> [[5]] 39 | 5. -> <5> 40 | '5 read -> [5] 41 | 1'a -> "1a" 42 | 43 | --/ Test no promotion 44 | 5[ -> AtlasTypeError 45 | 46 | -- Test promotion prefered when last op was vec 47 | "123".& -> <[1],[2],[3]> 48 | 49 | -- not escaped since not needed 50 | '\" --" -> "\\ --" 51 | '\'f -> "\\f" 52 | 53 | 1+() -> AtlasTypeError 54 | ()%2 -> AtlasTypeError 55 | 0-12 -> -12 56 | 0/12 -> 0 57 | 012 -> 12 58 | () -> [] 59 | () type -> "[A]" 60 | ();,()_() -> [[],[]] 61 | 62 | ()[ -> DynamicError 63 | ()] -> DynamicError 64 | 5;> -> [] 65 | 66 | ----------/ test vars 67 | 5@v1+v1 -> 10 68 | 69 | -- test nil 70 | ();,(),() -> [[],[],[]] 71 | ();,(),() type -> "[[A]]" 72 | 73 | -------- test infinite list 74 | v1`1@v1 -> [1,1,1,1,1,1,... 75 | v1`'-@v1 -> "-------------... 76 | 77 | -------------- test zips 78 | 3;,4+1 -> <4,5> 79 | 3;,4;,(5;,7)+1 -> <<4,5>,<6,8>> 80 | 1+"asdf"% -> "bteg" 81 | "asdf"+1 -> <'b,'t,'e,'g> 82 | (1;,2)+(4;,6,8) -> <5,8> 83 | (4;,6,8)+(1;,2) -> <5,8> 84 | 85 | "asdf"-1% -> "`rce" 86 | "abcd"-"aaa"% -> [0,1,2] 87 | 88 | "abcd","xzy" [ -> "abcd" 89 | "abcd","xzy".[% -> "ax" 90 | "abcd","xzy".]% -> "dy" 91 | "abcd","xzy"..[ -> AtlasTypeError 92 | 93 | "abcd";,"xzy" > -> ["xzy"] 94 | "abcd";,"xzy".tail -> <"bcd","zy"> 95 | 'c tail -> AtlasTypeError 96 | 97 | "def";,"xzy".`"abc" -> <"adef","bxzy"> 98 | "def","xzy"..;`"abc" -> <<"ad","be","cf">,<"ax","bz","cy">> 99 | 100 | -- coercion tests 101 | 'a | "asdf" -> <'a,'a,'a,'a> 102 | ' | "asdf" % -> "asdf" 103 | "asdf" | 'a% -> "asdf" 104 | "" | ('a;) -> "a" 105 | 0|"b" -> "b" 106 | ""|2 -> "2" 107 | 0|'c -> "c" 108 | 4,3,0|"f" -> <"4","3","f"> 109 | 0,1|("f","t") -> ["0","1"] 110 | ("f","t")|(0,1) -> ["f","t"] 111 | ()|1 -> <> 112 | ()|(1;) -> [1] 113 | ()|"a" -> "a" 114 | 115 | 0 & 2 | 3 -> 3 116 | 1 & 2 | 3 -> 2 117 | () & 2 | 3 -> 3 118 | 0; & 2 | 3 -> 2 119 | " "[ & 2 | 3 -> 3 120 | "a"[ & 2 | 3 -> 2 121 | 122 | 0 & 'a; -> " " 123 | () & 1 -> 0 124 | "" & "asdf" -> "" 125 | 126 | 1 & 'a | "b" -> <'a> 127 | 1 & 'a, | "bcd"% -> "aaa" 128 | "a " . & '1 | "23" % -> "13" 129 | 130 | "a b " . & ("fghi".) | ("jklm".) % -> "fkhm" 131 | "a b " . & 1 | 0 % -> [1,0,1,0] 132 | 133 | 1,2.p -> "<1,2>" 134 | 135 | "asdf"[(1,2) -> <"a","as"> 136 | "abc","123".[2 -> <"ab","12"> 137 | 138 | ---------- more advanced circular programming 139 | 1+v1%`1@v1 -> [1,2,3,4,5... 140 | v1+v2%`1@v2`1@v1 -> [1,1,2,3,5,8,13,21... 141 | v1`0+(1+v2%`1@v2)%@v1 -> [1,3,6,10,15... 142 | 1+v1@v1 -> AtlasTypeError 143 | 144 | ---- test more ops and zips 145 | 1, -> <1,1,1,1... 146 | "hi".,[5 -> <"hhhhh","iiiii"> 147 | 148 | "hi".; -> <"h","i"> 149 | "asdfg"]2 -> "dfg" 150 | "abc","123".]2 -> <"c","3"> 151 | 152 | "hi","there",("asdf","123")._ -> <"hithere","asdf123"> 153 | 1;,%_[5 -> [1,1,1,1,1] 154 | 155 | "abc"_("123",%_) -> "abc123123... 156 | "abc",%__"123" -> "abcabcabc... 157 | "123".;`"abc" -> <"a1","b2","c3"> 158 | "a","b"._("1","2") -> <"a1","b2"> 159 | 'a "b" -> "ab" 160 | 161 | "asdf"< -> "asd" 162 | "abc","123".< -> <"ab","12"> 163 | 164 | "abc","123"\ -> ["a1","b2","c3"] 165 | "abc","12","xyz"\ -> ["a1x","b2y","c"] 166 | "abc","123",("xyz","789").\ -> <["a1","b2","c3"],["x7","y8","z9"]> 167 | 168 | "abcd";\ -> ["a","b","c","d"] 169 | 4\ -> [[4]] 170 | "abc","123".;\ -> <["a","b","c"],["1","2","3"]> 171 | 172 | -- circular programming foldr 173 | 4,5,6+(v1>,0)@v1[ -> 15 174 | 175 | -- error and catching 176 | (v1<)`0@v1 -> InfiniteLoopError 177 | ""[ -> DynamicError 178 | --catch /9 :1:2:0:3;4 -> f 179 | 180 | "a b c".&(1,2,3,4,5,6.;)_ -> [1,3,5] 181 | 182 | ""& -> [] 183 | " "& -> [] 184 | "-a"& -> [] 185 | 186 | '5& -> [5] 187 | '5.& -> <[5]> 188 | 189 | "1 2","3 4"& -> <[1,2],[3,4]> 190 | 191 | -- complicated test (primes) 192 | (1+(v2*v1`1@v1))%(1+v2`2@v2) [20. & (();,) | (v2.;;)__ -> [2,3,5,7,11... 193 | 194 | v1+(1,2,3,(4,5,6))`(0,)@v1] -> <6,15> 195 | 196 | (); -> [[]] 197 | ();[ -> [] 198 | 199 | -- check incorrect faith attempt 200 | -- this would attempt to access invalid elements if said check was not in place 201 | 0;;._(v1+(3;;)%%@v2.)%&(4;;[0;)|(5;;[0;)[@v1 p _(v2 p) -> DynamicError 202 | 203 | -- tails' faith example that needed padding before 204 | a`0+1@a=10#&a|(b%>+1)@b[ -> 19 205 | 206 | -- Test auto replicating of nil 207 | "______MA_"='_ & ("CHISTMAS".;) | () _ -> "CHISTM" 208 | 209 | 210 | -- Test promotion 211 | "123".;& -> <[1],[2],[3]> 212 | "asdf"\ -> ["a","s","d","f"] 213 | 214 | 5. -> <5> 215 | 5% -> AtlasTypeError 216 | 217 | -- Test parse uses previous token correct 218 | 1 (2-) -> -2 219 | 220 | -- implicit value is circular program 221 | (+1`0)[5 -> [0,1,2,3,4] 222 | +(1,2,3,4)`0 -> [0,1,3,6,10] 223 | -+(1,2,3,4)`0 -> [0,1,1,2,2] 224 | 225 | 226 | -- This tests a pathological case in var lookups 227 | a@F_F@a -> InfiniteLoopError 228 | 229 | -- Using the unknown type 230 | a@a -> AtlasTypeError 231 | a@a;# -> 1 232 | 233 | 234 | -- infinite type 235 | a;@a -> AtlasTypeError 236 | 237 | 'a-1000 -> 'invalid char: -903 238 | 239 | -- test unbalanced ) for use with circular programming/nil 240 | ) -> [] 241 | `0+1)[5# -> 5 242 | 243 | -- test apply 244 | 2*3@+4 -> 14 245 | 2*3-@+4 -> 2 246 | 247 | 1+2@3 -> 7 248 | 249 | -- test flip 250 | 1\2 -> 2 251 | "a"\"b" -> "ba" 252 | 1`\(2,3) -> [1,2,3] 253 | 1-\-2 -> <<-3>> 254 | 255 | -- this would be type A which should not be possible to construct 256 | ""&a@a -> DynamicError 257 | 258 | (1@"") -> "1" 259 | 260 | -- test laziness of chunk while using collatz conjecture problem 261 | a%2&(3*a+1)|(a/2)`8@a~(a=.~%)[ -> [8,4,2,1] 262 | -------------------------------------------------------------------------------- /test/test_vec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../repl.rb' 2 | 3 | def check(expected,found,name,test) 4 | if expected!=found 5 | STDERR.puts "expecting %p found %p for %s of %p" % [expected,found,name,test] 6 | exit 7 | end 8 | end 9 | 10 | fn_type = create_specs({[Int]=>Int})[0] 11 | 12 | T = TypeWithVecLevel 13 | U = Unknown 14 | 15 | # todo test VecOf 16 | # what other things can gen errors? are many errors impossible? 17 | 18 | # => zip_level, rep_level, return type 19 | tests = { 20 | # [int] 21 | [[T.new(Int+0,0)],{[Int]=>Int}] => [0,[0],[1],"Int"], 22 | [[T.new(Int+0,1)],{[Int]=>Int}] => [0,[0],[0],"Int"], 23 | [[T.new(Int+1,0)],{[Int]=>Int}] => [0,[0],[0],"Int"], 24 | [[T.new(Int+1,1)],{[Int]=>Int}] => [1,[0],[0],""], 25 | [[T.new(Int+2,0)],{[Int]=>Int}] => [1,[0],[0],""], 26 | [[T.new(Int+2,1)],{[Int]=>Int}] => [2,[0],[0],"<>"], 27 | 28 | # [a] 29 | [[T.new(Int+0,0)],{[A]=>A}] => [0,[0],[1],"Int"], 30 | [[T.new(Int+0,1)],{[A]=>A}] => [0,[0],[0],"Int"], 31 | [[T.new(Int+1,0)],{[A]=>A}] => [0,[0],[0],"Int"], 32 | [[T.new(Int+1,1)],{[A]=>A}] => [1,[0],[0],""], 33 | [[T.new(Int+2,0)],{[A]=>A}] => [0,[0],[0],"[Int]"], 34 | [[T.new(Int+2,1)],{[A]=>A}] => [1,[0],[0],"<[Int]>"], 35 | 36 | # [int] [int] 37 | [[T.new(Int+0,0),T.new(Int+0,0)],{[[Int],[Int]]=>Int}] => [0,[0,0],[1,1],"Int"], 38 | [[T.new(Int+2,2),T.new(Int+0,0)],{[[Int],[Int]]=>Int}] => [3,[0,3],[0,1],"<<>>"], 39 | [[T.new(Int+1,0),T.new(Int+1,0)],{[[Int],[Int]]=>Int}] => [0,[0,0],[0,0],"Int"], 40 | [[T.new(Int+2,0),T.new(Int+1,0)],{[[Int],[Int]]=>Int}] => [1,[0,1],[0,0],""], 41 | [[T.new(Int+1,1),T.new(Int+1,0)],{[[Int],[Int]]=>Int}] => [1,[0,1],[0,0],""], 42 | [[T.new(Int+2,1),T.new(Int+1,0)],{[[Int],[Int]]=>Int}] => [2,[0,2],[0,0],"<>"], 43 | [[T.new(Int+1,1),T.new(Int+1,1)],{[[Int],[Int]]=>Int}] => [1,[0,0],[0,0],""], 44 | [[T.new(Int+2,1),T.new(Int+1,1)],{[[Int],[Int]]=>Int}] => [2,[0,1],[0,0],"<>"], 45 | 46 | # [a] [int] 47 | [[T.new(Int+1,0),T.new(Int+0,0)],{[[A],[Int]]=>A}] => [0,[0,0],[0,1],"Int"], 48 | [[T.new(Int+1,0),T.new(Int+1,0)],{[[A],[Int]]=>A}] => [0,[0,0],[0,0],"Int"], 49 | [[T.new(Int+1,1),T.new(Int+1,0)],{[[A],[Int]]=>A}] => [1,[0,1],[0,0],""], 50 | [[T.new(Int+2,0),T.new(Int+1,0)],{[[A],[Int]]=>A}] => [0,[0,0],[0,0],"[Int]"], 51 | [[T.new(Int+1,0),T.new(Int+2,0)],{[[A],[Int]]=>A}] => [1,[1,0],[0,0],""], 52 | [[T.new(Int+1,1),T.new(Int+2,0)],{[[A],[Int]]=>A}] => [1,[0,0],[0,0],""], 53 | [[T.new(Int+2,0),T.new(Int+2,0)],{[[A],[Int]]=>A}] => [1,[1,0],[0,0],"<[Int]>"], 54 | [[T.new(Int+2,1),T.new(Int+2,0)],{[[A],[Int]]=>A}] => [1,[0,0],[0,0],"<[Int]>"], 55 | 56 | # [a] [a] 57 | [[T.new(Int+1,0),T.new(Int+0,0)],{[[A],[A]]=>A}] => [0,[0,0],[0,1],"Int"], 58 | [[T.new(Int+1,0),T.new(Int+1,0)],{[[A],[A]]=>A}] => [0,[0,0],[0,0],"Int"], 59 | [[T.new(Int+0,1),T.new(Int+1,0)],{[[A],[A]]=>A}] => [0,[0,0],[0,0],"Int"], 60 | [[T.new(Int+2,0),T.new(Int+1,0)],{[[A],[A]]=>A}] => [1,[0,1],[0,0],""], 61 | [[T.new(Int+2,1),T.new(Int+1,0)],{[[A],[A]]=>A}] => [2,[0,2],[0,0],"<>"], 62 | [[T.new(Int+2,0),T.new(Int+2,0)],{[[A],[A]]=>A}] => [0,[0,0],[0,0],"[Int]"], 63 | [[T.new(Int+1,1),T.new(Int+2,0)],{[[A],[A]]=>A}] => [1,[0,0],[0,0],""], 64 | [[T.new(Int+2,1),T.new(Int+2,0)],{[[A],[A]]=>A}] => [1,[0,1],[0,0],"<[Int]>"], 65 | 66 | # [a] [b] 67 | [[T.new(Int+1,0),T.new(Int+0,0)],{[[A],[B]]=>A}] => [0,[0,0],[0,1],"Int"], 68 | [[T.new(Int+1,0),T.new(Int+1,0)],{[[A],[B]]=>A}] => [0,[0,0],[0,0],"Int"], 69 | [[T.new(Int+2,0),T.new(Int+1,0)],{[[A],[B]]=>A}] => [0,[0,0],[0,0],"[Int]"], 70 | [[T.new(Int+2,1),T.new(Int+1,0)],{[[A],[B]]=>A}] => [1,[0,1],[0,0],"<[Int]>"], 71 | [[T.new(Int+2,1),T.new(Int+1,0)],{[[A],[B]]=>A}] => [1,[0,1],[0,0],"<[Int]>"], 72 | [[T.new(Int+2,1),T.new(Int+1,1)],{[[A],[B]]=>A}] => [1,[0,0],[0,0],"<[Int]>"], 73 | 74 | # Unknown tests 75 | # [int] would all be failures during lookup type fn 76 | 77 | # a 78 | [[T.new(U+0,0)],{[A]=>A}] => [0,[0],[0],"A"], 79 | [[T.new(U+1,0)],{[A]=>A}] => [0,[0],[0],"A"], 80 | [[T.new(U+2,0)],{[A]=>A}] => [0,[0],[0],"[A]"], 81 | [[T.new(U+0,0+1)],{[A]=>A}] => [1,[0],[0],""], 82 | [[T.new(U+0,0+2)],{[A]=>A}] => [2,[0],[0],"<>"], 83 | [[T.new(U+2,0+2)],{[A]=>A}] => [2,[0],[0],"<<[A]>>"], 84 | 85 | # [a] [int] 86 | [[T.new(U+0,0),T.new(Int+0,0)],{[[A],[Int]]=>A}] => [0,[0,0],[0,1],"A"], 87 | [[T.new(U+0,0),T.new(Int+2,0)],{[[A],[Int]]=>A}] => [1,[1,0],[0,0],""], 88 | [[T.new(U+2,2),T.new(Int+2,0)],{[[A],[Int]]=>A}] => [2,[0,1],[0,0],"<<[A]>>"], 89 | 90 | # [a] [a] 91 | [[T.new(U+0,0),T.new(U+0,0)],{[[A],[A]]=>A}] => [0,[0,0],[0,0],"A"], 92 | [[T.new(U+1,0),T.new(U+0,0)],{[[A],[A]]=>A}] => [0,[0,0],[0,0],"A"], 93 | [[T.new(U+2,0),T.new(U+0,0)],{[[A],[A]]=>A}] => [0,[0,0],[0,0],"[A]"], 94 | [[T.new(U+1,0),T.new(U+1,0)],{[[A],[A]]=>A}] => [0,[0,0],[0,0],"A"], 95 | [[T.new(U+2,0),T.new(U+1,0)],{[[A],[A]]=>A}] => [0,[0,0],[0,0],"[A]"], 96 | [[T.new(U+2,0),T.new(U+2,0)],{[[A],[A]]=>A}] => [0,[0,0],[0,0],"[A]"], 97 | [[T.new(U+2,0),T.new(U+0,1)],{[[A],[A]]=>A}] => [1,[1,0],[0,0],"<[A]>"], 98 | 99 | [[T.new(U+0,0),T.new(Int+0,0)],{[[A],[A]]=>A}] => [0,[0,0],[0,1],"Int"], 100 | [[T.new(U+1,0),T.new(Int+0,0)],{[[A],[A]]=>A}] => [0,[0,0],[0,1],"Int"], 101 | [[T.new(U+2,0),T.new(Int+0,0)],{[[A],[A]]=>A}] => [1,[0,1],[0,1],""], 102 | [[T.new(U+0,0),T.new(Int+1,0)],{[[A],[A]]=>A}] => [0,[0,0],[0,0],"Int"], 103 | [[T.new(U+0,0),T.new(Int+2,0)],{[[A],[A]]=>A}] => [0,[0,0],[0,0],"[Int]"], 104 | [[T.new(U+2,0),T.new(Int+2,0)],{[[A],[A]]=>A}] => [0,[0,0],[0,0],"[Int]"], 105 | 106 | # [a] [b] todo but probably not needed 107 | 108 | } 109 | # tests = {tests.keys[-1] => tests[tests.keys[-1]]} 110 | 111 | tests.each{|k,v| 112 | arg_types, spec = *k 113 | node=IR.new 114 | node.args = arg_types.map{|a| 115 | r=IR.new 116 | r.type_with_vec_level = a 117 | r.op = Op.new 118 | r 119 | } 120 | node.op=Op.new 121 | fn_type = create_specs(spec)[0] 122 | # begin 123 | t = possible_types(node, fn_type) 124 | # rescue #AtlasError => e 125 | if node.last_error 126 | if v != nil 127 | STDERR.puts "not expecting error for %p"%[k] 128 | raise node.last_error 129 | end 130 | next 131 | end 132 | # end 133 | if v == nil 134 | STDERR.puts "expecting error for %p"%[k] 135 | STDERR.puts "found %p %p %p" % [t.inspect,node.zip_level,node.rep_levels] 136 | exit 137 | end 138 | ez,er,ep,et = *v 139 | check(et,t.inspect,"type",k) 140 | check(ez,node.zip_level,"zip_level",k) 141 | check(er,node.rep_levels,"rep level",k) 142 | check(ep,node.promote_levels,"promote level",k) 143 | } 144 | 145 | puts "PASS #{tests.size} vec tests" 146 | -------------------------------------------------------------------------------- /docs/circular.md: -------------------------------------------------------------------------------- 1 | # More Circular Programming 2 | 3 | This doc is an unfinished state 4 | 5 | ## Transpose 6 | 7 | How can we transpose a list defined as so? 8 | 9 | a=(1,2,3,4),(5,6,7,8) 10 | ────────────────────────────────── 11 | 1 2 3 4 12 | 5 6 7 8 13 | 14 | The first row will be the heads of each row of `a`, which can be gotten with `.` and `head` 15 | 16 | a=(1,2,3,4),(5,6,7,8) 17 | a.head 18 | ────────────────────────────────── 19 | 1 5 20 | 21 | Note the `.` which takes one arg and vectorizes it. Just `head` would have given the first row. You may have been expecting a column of 1 5 instead of a row, but the heads of each element is just a 1D list and so it displays as such. 22 | 23 | The next row should be the heads of the tails: 24 | 25 | a=(1,2,3,4),(5,6,7,8) 26 | a.tail head 27 | ────────────────────────────────── 28 | 2 6 29 | 30 | And the next row would be the head of the tail of the tails. So essentially to transpose we want the heads of the repeated tailings of a 2D list, which we can do with circular programming of course. 31 | 32 | a=(1,2,3,4),(5,6,7,8) 33 | tails=tails..tail%%`a 34 | ────────────────────────────────── 35 | 1 2 3 4 36 | 5 6 7 8 37 | 38 | 2 3 4 39 | 6 7 8 40 | 41 | 3 4 42 | 7 8 43 | 44 | 4 45 | 8 46 | 47 | 48 | 49 | 50 | 2:14 (tail) tail on empty list (DynamicError) 51 | 52 | Here the `..tail%%` means perform the tail operation two levels deep. See the Vectorization section for more info. 53 | 54 | It is worth mentioning that this output is a 3D list, which is really just a list of list of a list, there is nothing special about nested lists, they are just lists. The separators for output are different however which makes them display nicely. You can also use the `show` op to display things like Haskell's show function. 55 | 56 | Also note the error. It would occur for the same program in Haskell too: 57 | 58 | tails=map (map tail) (a:tails) 59 | 60 | Anytime we see something of the form `var = something : var` it is defining an infinite list. This list clearly can't be infinite though, hence the error. It can be avoided by taking elements of length equal to the first row. 61 | 62 | a=(1,2,3,4),(5,6,7,8) 63 | tails=tails..tail%%`a 64 | tails take (a head len) 65 | ────────────────────────────────── 66 | 1 2 3 4 67 | 5 6 7 8 68 | 69 | 2 3 4 70 | 6 7 8 71 | 72 | 3 4 73 | 7 8 74 | 75 | 4 76 | 8 77 | 78 | `const` is a simply a function that takes two args and returns the first. But since it is zippedWith, it has the effect shortening the first list if the ignored arg is shorter. `head a` is the length we want. 79 | 80 | I have some ideas about creating an op to catch errors and truncate lists, but for now this manual step is required. 81 | 82 | To get the transpose now we just need to take the heads of each list: 83 | 84 | a=(1,2,3,4),(5,6,7,8) 85 | tails=tails..tail%%`a 86 | tails take (a head len)..head 87 | ────────────────────────────────── 88 | 1 5 89 | 2 6 90 | 3 7 91 | 4 8 92 | 93 | ## Scan on 2D lists 94 | 95 | We've seen how to do scanl on a list, but how does it work on 2D lists? 96 | 97 | a=(1,2,3,4),(5,6,7,8) 98 | b=a+b%%`(0,%) 99 | b. take 10 100 | ────────────────────────────────── 101 | 0 0 0 0 0 0 0 0 0 0 102 | 1 2 3 4 103 | 6 8 10 12 104 | 105 | The same way, we just have to start with a list of 0s instead of one and unvectorize the cons. I did a `10 take` purely for display purposes. 106 | 107 | That was easy, but what if we wanted to do it on rows instead of columns without transposing twice? 108 | 109 | We can do a zipped append: 110 | 111 | a=(1,2,3,4),(5,6,7,8) 112 | b=a+b`0 113 | ────────────────────────────────── 114 | 0 1 3 6 10 115 | 0 5 11 18 26 116 | 117 | This doesn't work if we port it to Haskell! It infinitely loops: 118 | 119 | b=zipWith (:) (repeat 0) (zipWith(zipWith (+))a b) 120 | 121 | The reason is because the first zipWith needs to know that both args are non empty for the result to be non empty. And when checking if the second arg is empty, that depends on if both `a` and `b` are non empty. But checking if `b` is non empty, is the very thing we were trying to decided in the first place since it is the result of the first zipWith. `b` is non empty if `a` and `b` are non empty. Haskell deals with this in the simplest way, but in this particular case it is definitely not the most useful way. 122 | 123 | Essentially what we really want is the "greatest fixed point". In general the greatest fixed point isn't clearly defined or efficient to compute. In this case it is both though, and Atlas finds it. The way it works is if it finds that there is a self dependency on a zipWith result, it has "faith" the result will be non empty. Afterwards it checks that the result was in fact non empty, otherwise it throws an infinite loop error (which is what it would have done with out the faith attempt in the first place). I'd like to generalize this logic to work for finding any type of greatest fixed point, but it will be more difficult for other cases as it would require back tracking in some cases. 124 | 125 | ## Map and Nested Map 126 | 127 | It's not obvious that we don't need map to map. But it is quite easy to eliminate the need: just use the list you wish to map over where you would use the map arg. 128 | 129 | For example: 130 | 131 | let a = [1,2,3] 132 | in map (\i -> (i + 2) * 3) a 133 | 134 | In Atlas is: 135 | 136 | a = 1,2,3 137 | a+2*3 138 | ────────────────────────────────── 139 | 9 12 15 140 | 141 | If you need to use the map arg multiple times, that is fine. 142 | 143 | let a = [1,2,3] 144 | in map (\i -> i * (i - 1) / 2) 145 | 146 | In Atlas is: 147 | 148 | a = 1,2,3 149 | a*(a-1)/2 150 | ────────────────────────────────── 151 | 0 1 3 152 | 153 | This same idea also replaces the need for any explicit zipWith of a complex function. A zipWith of a complex function is just a series zipWiths of a simple function. This is one reason why languages like APL are so concise, they never need map or zipWith and these are extremely common operations. 154 | 155 | It is worth noting that if you are using an operation that could be applied at different depths (for example head of a 2D list), you will need to use `!`'s the proper number of times to apply them at the right depth. `*` never needs `!` since it can only be applied to scalars. 156 | 157 | Ok, so that's great, but this doesn't work if we need to do nested maps, for example generating a multiplication table: 158 | 159 | map (\i -> map (\j -> i*j) [1,2,3]) [1,2,3] 160 | 161 | Won't work directly: 162 | 163 | (1,2,3) * (1,2,3) 164 | ────────────────────────────────── 165 | 1 4 9 166 | The reason is because vectorization zips instead of doing a 'cartesian product'. 167 | 168 | Doing a cartesian product is easy though. We just replicate each list in a different dimension 169 | 170 | 1,2,3,% take 3 171 | " and " 172 | 1,2,3., take 3 173 | ────────────────────────────────── 174 | 1 2 3 175 | 1 2 3 176 | 1 2 3 177 | and 178 | 1 1 1 179 | 2 2 2 180 | 3 3 3 181 | 182 | `,` means repeat, but it could have been done using our circular technique for creating infinite lists. Also the `3 take` is not needed because each list will take the shorter of the two and they are replicated in different directions with the other dimension still being 3. So the final program can be: 183 | 184 | 1,2,3, * (1,2,3.,) 185 | ────────────────────────────────── 186 | 1 2 3 187 | 2 4 6 188 | 3 6 9 189 | 190 | This technique can do any degree of nesting with any dimension lists. Essentially you need to think of each operation in a computation taking place at a location in nD space, where n is the number of nested loops. And despite the name you can use a cartesian product on any operation not just multiplication. 191 | 192 | Note for code golfers, the left `,` isn't needed since it knows it needs a 2D list. We could have written 193 | 194 | 1,2,3*(1,2,3.,) 195 | ────────────────────────────────── 196 | 1 2 3 197 | 2 4 6 198 | 3 6 9 199 | 200 | or even shorter by pushing and popping the 1,2,3 201 | 202 | 1,2,3{.,*} 203 | ────────────────────────────────── 204 | 1 2 3 205 | 2 4 6 206 | 3 6 9 207 | 208 | 209 | 210 | ## Foldr continued 211 | 212 | One option around this is to define two different zipWiths, one that only checks if left is empty and a different version that only checks if the right is empty. So in this case we would need to use zipWithL which doesn't check if the right arg is empty, only the left. These zipWiths would throw an error if the opposite side was the shorter list (rather than truncate). This isn't ideal because it puts the burden on the user to choose the correct zipWith and it actually still doesn't work because `++` still needs to know when the left operand is empty, but it would work if Haskell constructed lists symmetrically with `++` as a primitive instead of cons. 213 | 214 | There is a solution though! We can define a function that adds an infinite list to the end of a list and construct it in such a way that it doesn't need to check anything if asked if empty, since it can never be empty even after tailing it. This allows zip's empty check to immediately succeed. Here is that function in Haskell: 215 | 216 | pad :: [a] -> a -> [a] 217 | pad a v = h : pad t v 218 | where 219 | ~(h,t) = 220 | if null a 221 | then (v,[]) 222 | else (head a,tail a) 223 | 224 | TODO update this with 1 arg version 225 | 226 | This function pads a list by adding an infinite list of a repeating element after it (e.g. `pad [1,2,3,4] 0` is `[1,2,3,4,0,0,0,...]`. But critically it starts by returning a list rather than first checking which case to return. 227 | 228 | Notice that it always returns `h : pad t v`, which is a non empty list, regardless of if `a` was empty or not, thus nothing needs to be computed when asked if the result is empty. It is only the contents of said list that depend on if `a` was empty. This is definitely not the most intuitive way to define this function, but it is the only way that is sufficiently lazy. 229 | 230 | Now we can write: 231 | 232 | a = [1,2,3,4] 233 | b = zipWith (+) a (tail (pad b 0)) 234 | 235 | And it works! 236 | -------------------------------------------------------------------------------- /lazylib.rb: -------------------------------------------------------------------------------- 1 | # Set this to default because these get used a bit during parse (e.g. creating a string) 2 | $step_limit=Float::INFINITY 3 | $reductions = 0 4 | 5 | def run(root,out=STDOUT,output_limit=10000,step_limit=Float::INFINITY) 6 | $step_limit = step_limit + $reductions 7 | print_string(make_promises(root), out, output_limit) 8 | end 9 | 10 | def make_promises(node) 11 | return node.promise if node.promise 12 | if node.op.no_zip 13 | arg_types = node.args.map(&:type_with_vec_level) 14 | else 15 | arg_types = node.args.zip(0..).map{|a,i|a.type + a.vec_level - node.zip_level + node.rep_levels[i] + node.promote_levels[i]} 16 | end 17 | args = nil 18 | node.promise = Promise.new { 19 | zipn(node.zip_level, args, node.op.impl[arg_types, node]) 20 | } 21 | args = node.args.zip(0..).map{|arg,i| 22 | promoted = Promise.new{zipn(arg.vec_level, [make_promises(arg)], ->a{promoten(a,node.promote_levels[i]).value})} 23 | repn(promoted,node.rep_levels[i]) 24 | } 25 | node.promise 26 | end 27 | 28 | class Promise 29 | attr_accessor :expect_non_empty 30 | def initialize(&block) 31 | @impl=block 32 | end 33 | def empty 34 | value==[] 35 | end 36 | def value 37 | raise DynamicError.new "step limit exceeded",nil if $reductions > $step_limit 38 | $reductions+=1 39 | if Proc===@impl 40 | begin 41 | raise InfiniteLoopError.new "infinite loop detected",self,nil if @calculating # todo fix from location 42 | @calculating=true 43 | @impl=@impl[] 44 | ensure 45 | # not really needed since new promises are created rather than reused 46 | @calculating=false 47 | end 48 | raise DynamicError.new "infinite loop was assumed to be non empty, but was nonempty",nil if expect_non_empty && @impl == [] 49 | end 50 | @impl 51 | end 52 | alias by value 53 | end 54 | 55 | class By 56 | def initialize(value,by_value) 57 | @value=value 58 | @by_value=by_value 59 | end 60 | def by 61 | @by_value 62 | end 63 | def empty 64 | @value == [] 65 | end 66 | def value 67 | @value 68 | end 69 | end 70 | 71 | # Use this to avoid creating promises that are pointless because the value is constant or it will immediately be computed after construction. 72 | class Const < Struct.new(:value) 73 | def empty 74 | value==[] 75 | end 76 | alias by value 77 | end 78 | class Object 79 | def const 80 | Const.new(self) 81 | end 82 | end 83 | 84 | def take(n, a) 85 | return [] if n <= 0 || a.empty 86 | [a.value[0], Promise.new{ take(n-1, a.value[1]) }] 87 | end 88 | 89 | def drop(n, a) 90 | while n>0 && !a.empty 91 | n-=1 92 | a=a.value[1] 93 | end 94 | a.value 95 | end 96 | 97 | def range(a,b) 98 | return [] if a>=b 99 | [a.const, Promise.new{range(a+1,b)}] 100 | end 101 | 102 | def range_from(a) 103 | [a.const, Promise.new{range_from(a+1)}] 104 | end 105 | 106 | # this isn't as lazy as possible, but it gets to use hashes 107 | def occurence_count(a,h=Hash.new(-1)) 108 | return [] if a.empty 109 | [(h[to_strict_list(a.value[0])]+=1).const, Promise.new{occurence_count(a.value[1], h)}] 110 | end 111 | 112 | def filter(a,b,b_elem_type) 113 | return [] if a.empty || b.empty 114 | if truthy(b_elem_type,b.value[0]) 115 | [a.value[0],Promise.new{ filter(a.value[1],b.value[1],b_elem_type) }] 116 | else 117 | filter(a.value[1],b.value[1],b_elem_type) 118 | end 119 | end 120 | 121 | def sortby(a,b,t) 122 | sort(toby(a,b),t) 123 | end 124 | 125 | def toby(a,b) 126 | return Null if a.empty || b.empty 127 | Promise.new{ [By.new(a.value[0].value, b.value[0].value), toby(a.value[1], b.value[1])] } 128 | end 129 | 130 | # It would be very interesting and useful to design a more lazy sorting algorithm 131 | # so that you can select ith element in O(n) total time after sorting a list. 132 | def sort(a,t) 133 | return [] if a.empty 134 | return a.value if a.value[1].empty 135 | evens,odds=partition(a) 136 | merge(sort(evens,t),sort(odds,t),t) 137 | end 138 | 139 | # This is not lazy at all, but should be ok since used by sort which consumes it right away 140 | def partition(a) 141 | return [Null,Null] if a.empty 142 | evens,odds=partition(a.value[1]) 143 | [[a.value[0],odds].const,evens] 144 | end 145 | 146 | def merge(a,b,t) 147 | return b if a==[] 148 | return a if b==[] 149 | if spaceship(a[0], b[0],t) < 0 150 | [a[0], Promise.new{merge(a[1].value,b,t)}] 151 | else 152 | [b[0], Promise.new{merge(a,b[1].value,t)}] 153 | end 154 | end 155 | 156 | def to_strict_list(a,sofar=[]) 157 | a=a.value 158 | return a if !(Array===a) 159 | return sofar if a==[] 160 | to_strict_list(a[1],sofar< (value -> Promise) -> value 224 | def map(a,&b) 225 | a.empty ? [] : [Promise.new{b[a.value[0]]}, Promise.new{map(a.value[1],&b)}] 226 | end 227 | 228 | # value -> value 229 | # truncate as soon as encounter empty list 230 | def trunc(a) 231 | a.empty || a.value[0].empty ? [] : [a.value[0], Promise.new{trunc(a.value[1])}] 232 | end 233 | 234 | def transpose(a) 235 | return [] if a.empty 236 | return transpose(a.value[1]) if a.value[0].empty 237 | broken = trunc(a.value[1]).const 238 | hds = Promise.new{ map(broken){|v|v.value[0].value} } 239 | tls = Promise.new{ map(broken){|v|v.value[1].value} } 240 | [Promise.new{[a.value[0].value[0],hds]}, 241 | Promise.new{transpose [a.value[0].value[1],tls].const}] 242 | end 243 | 244 | def last(a) 245 | prev=nil 246 | until a.empty 247 | prev = a 248 | a = a.value[1] 249 | end 250 | raise DynamicError.new("empty last", nil) if prev == nil 251 | prev.value[0].value 252 | end 253 | 254 | # n = int number of dims to zip 255 | # a = args, [promise] 256 | # f = impl, promises -> value 257 | # returns value 258 | def zipn(n,a,f) 259 | return f[*a] if n <= 0 || a==[] 260 | faith = [] 261 | return [] if a.any?{|i| 262 | begin 263 | i.empty 264 | rescue InfiniteLoopError => e 265 | # gotta have faith 266 | # solves this type of problem: a=!:,0 +a ::1;2;:3;4 267 | faith << i 268 | false # not empty, for now... 269 | end 270 | } 271 | faith.each{|i| i.expect_non_empty = true } 272 | [Promise.new{zipn(n-1,a.map{|i|Promise.new{i.value[0].value}},f) }, 273 | Promise.new{zipn(n,a.map{|i|Promise.new{i.value[1].value}},f) }] 274 | end 275 | 276 | def repeat(a) 277 | ret = [a] 278 | ret << Promise.new{ret} 279 | ret 280 | end 281 | 282 | def repn(a,n) 283 | if n<=0 284 | a 285 | else 286 | Promise.new{repeat(repn(a,n-1))} 287 | end 288 | end 289 | 290 | def promoten(a,n) 291 | if n<=0 292 | a 293 | else 294 | Promise.new{[promoten(a,n-1),Null]} 295 | end 296 | end 297 | 298 | # value -> value -> value 299 | def spaceship(a,b,t) 300 | if t.dim>0 301 | return 0 if a.empty && b.empty 302 | return -1 if a.empty 303 | return 1 if b.empty 304 | s0 = spaceship(a.by[0],b.by[0],t-1) 305 | return s0 if s0 != 0 306 | return spaceship(a.by[1],b.by[1],t) 307 | else 308 | a.by<=>b.by 309 | end 310 | end 311 | 312 | def len(a) 313 | return 0 if a.empty 314 | return 1+len(a.value[1]) 315 | end 316 | 317 | # value -> Promise -> value 318 | def append(v,r) 319 | v.empty ? r.value : [v.value[0],Promise.new{append(v.value[1], r)}] 320 | end 321 | 322 | # promise -> promise -> bool -> (value -> promise -> ... -> value) -> value 323 | def concat_map(v,rhs,first=true,&b) 324 | if v.empty 325 | rhs.value 326 | else 327 | b[v.value[0],Promise.new{concat_map(v.value[1],rhs,false,&b)},first] 328 | end 329 | end 330 | 331 | def concat(a) 332 | concat_map(a,Null){|i,r,first|append(i,r)} 333 | end 334 | 335 | def inspect_value(t,value,zip_level) 336 | inspect_value_h(t,value,Null,zip_level) 337 | end 338 | 339 | def inspect_value_h(t,value,rhs,zip_level) 340 | if t==Str && zip_level <= 0 341 | ['"'.ord.const, Promise.new{ 342 | concat_map(value,Promise.new{str_to_lazy_list('"',rhs)}){|v,r,first| 343 | str_to_lazy_list(escape_str_char(v.value),r) 344 | } 345 | }] 346 | elsif t==Int 347 | str_to_lazy_list(value.value.to_s,rhs) 348 | elsif t==Char 349 | str_to_lazy_list(inspect_char(value.value),rhs) 350 | else #List 351 | [(zip_level>0?"<":"[").ord.const, Promise.new{ 352 | concat_map(value,Promise.new{str_to_lazy_list((zip_level>0?">":"]"),rhs)}){|v,r,first| 353 | first ? 354 | inspect_value_h(t-1,v,r,zip_level-1) : 355 | [','.ord.const,Promise.new{inspect_value_h(t-1,v,r,zip_level-1)}] 356 | } 357 | }] 358 | end 359 | end 360 | 361 | # convert a from int to str if tb == str and ta == int, but possibly vectorized 362 | def coerce2s(ta, a, tb) 363 | return a if ta==tb || tb.is_unknown || ta.is_unknown #?? 364 | case [ta.base_elem,tb.base_elem] 365 | when [:int,:char] 366 | raise if ta.dim+1 != tb.dim 367 | return Promise.new{zipn(ta.dim,[a],->av{str_to_lazy_list(av.value.to_s)})} 368 | when [:char,:int] 369 | raise if ta.dim != tb.dim+1 370 | return a 371 | else 372 | raise "coerce of %p %p not supported"%[ta,tb] 373 | end 374 | end 375 | 376 | def to_string(t, value) 377 | to_string_h(t,value,t.string_dim, Null) 378 | end 379 | 380 | def to_string_h(t, value, orig_dim, rhs) 381 | if t == Int 382 | inspect_value_h(t, value, rhs, 0) 383 | elsif t == Char 384 | [value, rhs] 385 | else # List 386 | dim = t.string_dim 387 | # print newline separators after every element for better interactive io 388 | separator1 = dim == 2 ? "\n" : "" 389 | # but don't do this for separators like space, you would end up with trailing space in output 390 | separator2 = [""," ",""][dim] || "\n" 391 | 392 | # this would make the lang a bit better on golf.shinh.org but not intuitive 393 | #separator = "\n" if orig_dim == 1 && dim == 1 394 | 395 | concat_map(value,rhs){|v,r,first| 396 | svalue = Promise.new{ to_string_h(t-1, v, orig_dim, Promise.new{str_to_lazy_list(separator1, r)}) } 397 | first ? svalue.value : str_to_lazy_list(separator2, svalue) 398 | } 399 | end 400 | end 401 | 402 | def print_string(value, out, limit) 403 | begin 404 | while !value.empty && limit > 0 405 | c = value.value[0].value 406 | $last_was_newline = c == 10 407 | out.print "%c" % c 408 | value = value.value[1] 409 | limit -= 1 410 | end 411 | rescue ArgumentError 412 | raise DynamicError.new "invalid character for printing ordinal value: %d" % value.value[0].value, nil 413 | end 414 | end 415 | 416 | def read_int(s) 417 | multiplier=1 418 | until s.empty || s.value[0].value.chr =~ /[0-9]/ 419 | if s.value[0].value == ?-.ord 420 | multiplier *= -1 421 | else 422 | multiplier = 1 423 | end 424 | s = s.value[1] 425 | end 426 | v = 0 427 | found_int = false 428 | until s.empty || !(s.value[0].value.chr =~ /[0-9]/) 429 | found_int = true 430 | v = v*10+s.value[0].value-48 431 | s = s.value[1] 432 | end 433 | [multiplier * v, found_int, s] 434 | end 435 | 436 | # string value -> [string] value 437 | def lines(s) 438 | return [] if s.empty 439 | 440 | after = Promise.new{lines(s.value[1])} 441 | if s.value[0].value == 10 442 | [Null, after] 443 | else 444 | [Promise.new{ 445 | after.empty ? [s.value[0], Null] : [s.value[0], after.value[0]] 446 | }, 447 | Promise.new{ 448 | after.empty ? [] : after.value[1].value 449 | }] 450 | end 451 | end 452 | 453 | # string promise -> [int] value 454 | def split_non_digits(s) 455 | return [] if s.empty 456 | v,found,s2=read_int(s) 457 | return [] if !found 458 | [v.const,Promise.new{split_non_digits(s2)}] 459 | end 460 | 461 | ReadStdin = Promise.new{ read_stdin } 462 | def read_stdin 463 | c=STDIN.getc 464 | if c.nil? 465 | [] 466 | else 467 | [c.ord.const, Promise.new{ read_stdin }] 468 | end 469 | end 470 | 471 | Null = [].const 472 | 473 | def str_to_lazy_list(s,rhs=Null) 474 | to_lazy_list(s.chars.map(&:ord), rhs) 475 | end 476 | 477 | def to_lazy_list(l, rhs=Null, ind=0) 478 | ind >= l.size ? rhs.value : [l[ind].const, Promise.new{to_lazy_list(l, rhs, ind+1)}] 479 | end 480 | 481 | def truthy(type, value) 482 | if type == Int 483 | value.value > 0 484 | elsif type == Char 485 | !!value.value.chr[/\S/] 486 | else # List 487 | !value.empty 488 | end 489 | end 490 | -------------------------------------------------------------------------------- /ops.rb: -------------------------------------------------------------------------------- 1 | require_relative "./type.rb" 2 | require_relative "./spec.rb" 3 | 4 | MacroImpl = -> *args { raise "macro impl called" } 5 | 6 | class Op < Struct.new( 7 | :name, 8 | :sym, # optional 9 | :type, 10 | :type_summary, 11 | :examples, 12 | :desc, 13 | :no_promote, 14 | :no_zip, 15 | :impl, 16 | :tests) 17 | def narg 18 | type ? type[0].specs.size : 0 19 | end 20 | def help(show_everything=true) 21 | puts "#{name} #{sym}" 22 | puts desc if desc 23 | if type_summary 24 | puts type_summary 25 | else 26 | type.each{|t| 27 | puts t.inspect.gsub('->','→').gsub('[Char]','Str') 28 | } 29 | end 30 | (examples+tests*(show_everything ? 1 : 0)).each{|example| 31 | puts example.gsub('->','→') 32 | } 33 | misc = [] 34 | puts "no_zip=true" if no_zip 35 | puts 36 | end 37 | def add_test(s) 38 | tests< arg_types,from { poly_impl[*arg_types] } 62 | elsif impl_with_loc 63 | built_impl = -> arg_types,from { impl_with_loc[from] } 64 | elsif final_impl 65 | built_impl = final_impl 66 | else 67 | built_impl = -> arg_types,from { Proc===impl ? impl : lambda { impl } } 68 | end 69 | examples = [] 70 | examples << example if example 71 | 72 | if coerce 73 | f = built_impl 74 | built_impl = -> t,from { -> a,b { f[t,from][coerce2s(t[0],a,t[1]),coerce2s(t[1],b,t[0])] }} 75 | end 76 | 77 | Op.new(name,sym,type,type_summary,examples,desc,no_promote,no_zip,built_impl,[]) 78 | end 79 | 80 | def int_col(n) 81 | -> { 82 | map(lines(ReadStdin).const){|v| 83 | take(1,Promise.new{drop(n,Promise.new{split_non_digits(v)})}) 84 | } 85 | } 86 | end 87 | 88 | OpsList = [ 89 | create_op( 90 | name: "head", 91 | sym: "[", 92 | example: '"abc"[ -> \'a', 93 | type: { [A] => A }, 94 | no_promote: true, 95 | impl_with_loc: -> from { -> a { 96 | raise DynamicError.new "head on empty list",from if a.empty 97 | a.value[0].value 98 | }}, 99 | ), create_op( 100 | name: "last", 101 | sym: "]", 102 | no_promote: true, 103 | example: '"abc"] -> \'c', 104 | type: { [A] => A }, 105 | impl_with_loc: -> from { -> a { 106 | raise DynamicError.new "last on empty list",from if a.empty 107 | last(a) 108 | }} 109 | ), create_op( 110 | name: "tail", 111 | example: '"abc"> -> "bc"', 112 | sym: ">", 113 | no_promote: true, 114 | type: { [A] => [A] }, 115 | impl_with_loc: -> from { -> a { 116 | raise DynamicError.new "tail on empty list",from if a.empty 117 | a.value[1].value}} 118 | ), create_op( 119 | name: "init", 120 | example: '"abc"< -> "ab"', 121 | sym: "<", 122 | no_promote: true, 123 | type: { [A] => [A] }, 124 | impl_with_loc: -> from { -> a { 125 | raise DynamicError.new "init on empty list",from if a.empty 126 | init(a) 127 | }} 128 | ), create_op( 129 | name: "add", 130 | sym: "+", 131 | example: "1+2 -> 3", 132 | type: { [Int,Int] => Int, 133 | [Int,Char] => Char, 134 | [Char,Int] => Char }, 135 | impl: -> a,b { a.value + b.value }) 136 | .add_test("'a+1 -> 'b"), 137 | create_op( 138 | name: "sub", 139 | sym: "-", 140 | example: '5-3 -> 2', 141 | type: { [Int,Int] => Int, 142 | [Char,Int] => Char, 143 | [Char,Char] => Int }, 144 | impl: -> a,b { a.value - b.value } 145 | ), create_op( 146 | name: "mult", 147 | example: '2*3 -> 6', 148 | sym: "*", 149 | type: { [Int,Int] => Int }, 150 | impl: -> a,b { a.value * b.value } 151 | ), create_op( 152 | name: "join", 153 | example: '"hi","yo"*" " -> "hi yo"', 154 | sym: "*", 155 | type: { [[Str],Str] => Str, 156 | [[Int],Str] => Str,}, 157 | poly_impl: -> at,bt { -> a,b { join(coerce2s(at,a,Str+1),b) } }) 158 | .add_test('1,2,3*", " -> "1, 2, 3"'), 159 | create_op( 160 | name: "split", 161 | example: '"hi, yo"/", " -> ["hi","yo"]', 162 | sym: "/", 163 | type: { [Str,Str] => [Str] }, 164 | impl: -> a,b { split(a,b) }) 165 | .add_test('"abcbcde"/"bcd" -> ["abc","e"]') 166 | .add_test('"ab",*" "/"b "[2 -> ["a","a"]') # test laziness 167 | .add_test('",a,,b,"/"," -> ["a","b"]'), 168 | create_op( 169 | name: "pow", 170 | example: '2^3 -> 8', 171 | sym: "^", 172 | type: { [Int,Int] => Int }, 173 | impl: -> a,b { a.value ** b.value } # todo use formula that will always be int 174 | ), create_op( 175 | name: "replicate", 176 | example: '"ab"^3 -> "ababab"', 177 | sym: "^", 178 | type: { [Str,Int] => Str }, 179 | impl: -> a,b { concat(take(b.value,repeat(a).const).const) } 180 | ), create_op( 181 | name: "div", 182 | example: '7/3 -> 2', 183 | sym: "/", 184 | type: { [Int,Int] => Int }, 185 | impl_with_loc: -> from { -> a,b { 186 | if b.value==0 187 | raise DynamicError.new("div 0", from) # todo maybe too complicated to be worth it same for mod 188 | else 189 | a.value/b.value 190 | end 191 | }}) 192 | .add_test("10/5 -> 2") 193 | .add_test("9/5 -> 1") 194 | .add_test("11/(5-) -> -3") 195 | .add_test("10/(5-) -> -2") 196 | .add_test("11-/5 -> -3") 197 | .add_test("10-/5 -> -2") 198 | .add_test("10-/(5-) -> 2") 199 | .add_test("9-/(5-) -> 1") 200 | .add_test("1/0 -> DynamicError") 201 | .add_test("0/0 -> DynamicError"), 202 | create_op( 203 | name: "mod", 204 | example: '7%3 -> 1', 205 | sym: "%", 206 | type: { [Int,Int] => Int }, 207 | impl_with_loc: -> from { -> a,b { 208 | if b.value==0 209 | raise DynamicError.new("mod 0",from) 210 | else 211 | a.value % b.value 212 | end 213 | }}) 214 | .add_test("10%5 -> 0") 215 | .add_test("9%5 -> 4") 216 | .add_test("11%(5-) -> -4") 217 | .add_test("10%(5-) -> 0") 218 | .add_test("11-%5 -> 4") 219 | .add_test("10-%5 -> 0") 220 | .add_test("10-%(5-) -> 0") 221 | .add_test("9-%(5-) -> -4") 222 | .add_test("5%0 -> DynamicError"), 223 | create_op( 224 | name: "not", 225 | sym: "~", 226 | type: { A => Int }, 227 | example: '2,0.~ -> <0,1>', 228 | poly_impl: -> ta { -> a { truthy(ta,a) ? 0 : 1 } } 229 | ), create_op( 230 | name: "neg", 231 | sym: "-", 232 | type: { Int => Int }, 233 | example: '2- -> -2', 234 | impl: -> a { -a.value } 235 | ), create_op( 236 | name: "abs", 237 | sym: "|", 238 | type: { Int => Int }, 239 | example: '2-,3| -> <2,3>', 240 | impl: -> a { a.value.abs } 241 | ), create_op( 242 | name: "read", 243 | sym: "&", 244 | type: { Str => [Int] }, 245 | example: '"1 2 -3"& -> [1,2,-3]', 246 | impl: -> a { split_non_digits(a) }) 247 | .add_test('"1 2 -3 4a5 - -6 --7" & -> [1,2,-3,4,5,-6,7]'), 248 | create_op( 249 | name: "repeat", 250 | sym: ",", 251 | example: '2, -> <2,2,2,2,2...', 252 | type: { A => VecOf.new(A) }, 253 | impl: -> a { repeat(a) } 254 | ), create_op( 255 | name: "eq", 256 | example: '3=3 -> [3]', 257 | sym: "=", 258 | type: { [A,A] => [A] }, 259 | poly_impl: -> ta,tb {-> a,b { spaceship(a,b,ta) == 0 ? [b,Null] : [] } }) 260 | .add_test("3=2 -> []") 261 | .add_test("1=2 -> []") 262 | .add_test("1=1 -> [1]") 263 | .add_test('\'a=\'a -> "a"') 264 | .add_test("'d=100 -> AtlasTypeError") 265 | .add_test('"abc"="abc" -> ["abc"]') 266 | .add_test('"abc"="abd" -> []') 267 | .add_test('"abc"=\'a -> <"a","","">') 268 | .add_test('"abc"=(\'a.) -> <"a">') 269 | .add_test('"abc".="abd" -> <"a","b","">'), 270 | create_op( 271 | name: "lessThan", 272 | example: '4<5 -> [5]', 273 | sym: "<", 274 | type: { [A,A] => [A] }, 275 | poly_impl: -> ta,tb {-> a,b { spaceship(a,b,ta) == -1 ? [b,Null] : [] } } 276 | ).add_test("5<4 -> []"), 277 | create_op( 278 | name: "greaterThan", 279 | example: '5>4 -> [4]', 280 | sym: ">", 281 | type: { [A,A] => [A] }, 282 | poly_impl: -> ta,tb {-> a,b { spaceship(a,b,ta) == 1 ? [b,Null] : [] } } 283 | ).add_test("4>5 -> []"), 284 | create_op( 285 | name: "len", 286 | example: '"asdf"# -> 4', 287 | sym: "#", 288 | type: { [A] => Int }, 289 | impl: -> a { len(a) } 290 | ), create_op( 291 | name: "and", 292 | sym: "&", 293 | example: '1&2,(0&2) -> [2,0]', 294 | type: { [A,B] => B }, 295 | poly_impl: ->ta,tb { -> a,b { truthy(ta,a) ? b.value : tb.default_value }} 296 | ).add_test("0&2 -> 0"), 297 | create_op( 298 | name: "or", 299 | sym: "|", 300 | example: '1|2,(0|2) -> [1,2]', 301 | type: { [A,A] => A, 302 | [Aint,[Achar]] => [Achar], 303 | [[Achar],Aint] => [Achar] }, 304 | type_summary: "a a -> a (coerces)", 305 | poly_impl: ->ta,tb { -> a,b { truthy(ta,a) ? coerce2s(ta,a,tb).value : coerce2s(tb,b,ta).value }}, 306 | ).add_test("0|2 -> 2") 307 | .add_test('1|"b" -> "1"') 308 | .add_test('"b"|3 -> "b"') 309 | .add_test('0|"b" -> "b"') 310 | .add_test('""|2 -> "2"') 311 | .add_test(' 0|\'c -> "c"'), 312 | create_op( 313 | name: "input", 314 | desc: "all lines of stdin", 315 | sym: "$", 316 | type: VecOf.new(Str), 317 | impl: -> { lines(ReadStdin) } 318 | ), create_op( 319 | name: "show", 320 | sym: "p", 321 | example: '12p -> "12"', 322 | type: { A => Str }, 323 | no_zip: true, 324 | poly_impl: -> t { -> a { inspect_value(t.type+t.vec_level,a,t.vec_level) } } 325 | ).add_test('"a"p -> "\"a\""') 326 | .add_test('\'a p -> "\'a"') 327 | .add_test('1;p -> "[1]"'), 328 | create_op( 329 | name: "str", 330 | sym: "`", 331 | example: '12` -> "12"', 332 | type: { Int => Str }, 333 | impl: -> a { inspect_value(Int,a,0) } 334 | ), create_op( 335 | name: "single", 336 | sym: ";", 337 | example: '2; -> [2]', 338 | type: { A => [A] }, 339 | impl: -> a { [a,Null] } 340 | ), create_op( 341 | name: "take", 342 | sym: "[", 343 | example: '"abcd"[3 -> "abc"', 344 | type: { [[A],Int] => [A] }, 345 | impl: -> a,b { take(b.value, a) } 346 | ).add_test('"abc"[(2-) -> ""') 347 | .add_test('""[2 -> ""'), 348 | create_op( 349 | name: "drop", 350 | sym: "]", 351 | example: '"abcd"]3 -> "d"', 352 | type: { [[A],Int] => [A] }, 353 | impl: -> a,b { drop(b.value, a) } 354 | ).add_test('"abc"](2-) -> "abc"') 355 | .add_test('""]2 -> ""'), 356 | create_op( 357 | name: "range", 358 | sym: ":", 359 | example: '3:7 -> <3,4,5,6>', 360 | type: { [Int,Int] => VecOf.new(Int), 361 | [Char,Char] => VecOf.new(Char) }, 362 | impl: -> a,b { range(a.value, b.value) } 363 | ), create_op( 364 | name: "from", 365 | sym: ":", 366 | example: '3: -> <3,4,5,6,7,8...', 367 | type: { Int => VecOf.new(Int), 368 | Char => VecOf.new(Char) }, 369 | impl: -> a { range_from(a.value) } 370 | ), create_op( 371 | name: "count", 372 | sym: "=", 373 | example: '"abcaab" count -> [0,0,0,1,2,1]', 374 | type: { [A] => [Int] }, 375 | impl: -> a { occurence_count(a) } 376 | ).add_test('"ab","a","ab" count -> [0,0,1]'), 377 | create_op( 378 | name: "filter", 379 | sym: "?", 380 | example: '"abcd" ? (0,1,1,0) -> "bc"', 381 | type: { [[A],[B]] => [A] }, 382 | poly_impl: -> at,bt { -> a,b { filter(a,b,bt-1) }} 383 | ), create_op( 384 | name: "sort", 385 | sym: "!", 386 | example: '"atlas" ! -> "aalst"', 387 | type: { [A] => [A] }, 388 | poly_impl: -> at { -> a { sort(a,at-1) }} 389 | ), create_op( 390 | name: "sortBy", 391 | sym: "!", 392 | example: '"abc" ! (3,1,2) -> "bca"', 393 | type: { [[A],[B]] => [A] }, 394 | poly_impl: -> at,bt { -> a,b { sortby(a,b,bt-1) }}) 395 | .add_test('1,2,3 ! ("hi","there") -> [1,2]'), 396 | create_op( 397 | name: "chunkWhile", 398 | desc: "chunk while second arg is truthy, resulting groups are of the form [truthy, falsey]", 399 | sym: "~", 400 | example: '"abcd" ~ "11 1" -> ["ab","cd"]', 401 | type: { [[A],[B]] => [[A]] }, 402 | poly_impl: -> at,bt { -> a,b { chunk_while(a,b,bt-1) } }) 403 | .add_test('"abcde" ~ " 11 " -> ["","abc","d","e"]') 404 | .add_test('""~() -> [""]'), 405 | create_op( 406 | name: "concat", 407 | sym: "_", 408 | no_promote: true, 409 | example: '"abc","123"_ -> "abc123"', 410 | type: { [[A]] => [A] }, 411 | impl: -> a { concat(a) }, 412 | ), create_op( 413 | name: "implicitMult", 414 | sym: " ", 415 | example: '2 3 -> 6', 416 | type: { [Int,Int] => Int }, 417 | impl: -> a,b { a.value*b.value } 418 | ), create_op( 419 | name: "implicitAppend", 420 | sym: " ", 421 | example: '1"a" -> "1a"', 422 | type: { [[Achar],[Achar]] => [Achar], 423 | [Aint,[Achar]] => [Achar], 424 | [[Achar],Aint] => [Achar] }, 425 | type_summary: "[a] [a] -> [a] (coerces, one must be non int)", 426 | impl: -> a,b { append(a,b) }, 427 | coerce: true) 428 | .add_test("'a 'b -> \"ab\"") 429 | .add_test('"ab","cd" "e" -> <"abe","cde">') 430 | .add_test('("ab";) ("e";) -> ["ab","e"]'), 431 | create_op( 432 | name: "append", 433 | sym: "_", 434 | example: '"abc"_"123" -> "abc123"', 435 | type: { [[A],[A]] => [A], 436 | [Aint,[Achar]] => [Achar], 437 | [[Achar],Aint] => [Achar] }, 438 | type_summary: "[a] [a] -> [a] (coerces)", 439 | impl: -> a,b { append(a,b) }, 440 | coerce: true) 441 | .add_test('1_"a" -> "1a"'), 442 | create_op( 443 | name: "cons", 444 | sym: "`", 445 | example: '"abc"`\'d -> "dabc"', 446 | type: { [[A],A] => [A], 447 | [Aint,Achar] => [Achar], 448 | [[[Achar]],Aint] => [[Achar]] }, 449 | type_summary: "[a] a -> a (coerces)", 450 | poly_impl: -> ta,tb {-> a,b { [coerce2s(tb,b,ta-1),coerce2s(ta,a,tb+1)] }}) 451 | .add_test('\'a`5 -> ["5","a"]') 452 | .add_test('"a"`(5) -> ["5","a"]') 453 | .add_test('"a";;`(5;) -> [["5"],["a"]]') 454 | .add_test('5`\'a -> "a5"') 455 | .add_test('5;`"a" -> ["a","5"]') 456 | .add_test('\'b`\'a -> "ab"'), 457 | create_op( 458 | name: "snoc", 459 | desc: "rear cons, promote of first arg is allowed for easy list construction", 460 | sym: ",", 461 | example: '1,2,3 -> [1,2,3]', 462 | type: { [[A],A] => [A], 463 | [Aint,Achar] => [Achar], 464 | [[[Achar]],Aint] => [[Achar]] }, 465 | type_summary: "[a] a -> a (coerces)", 466 | poly_impl: -> ta,tb {-> a,b { 467 | append(coerce2s(ta,a,tb+1),[coerce2s(tb,b,ta-1),Null].const) }} 468 | ).add_test("2,1 -> [2,1]") 469 | .add_test('(2,3),1 -> [2,3,1]') 470 | .add_test('(2,3),(4,5),1 -> <[2,3,1],[4,5,1]>') 471 | .add_test('2,(1,0) -> [[2],[1,0]]') 472 | .add_test('(2,3),(1,0) -> [[2,3],[1,0]]') 473 | .add_test('(2,3).,1 -> <[2,1],[3,1]>') 474 | .add_test('(2,3),(4,5).,1 -> <[2,3,1],[4,5,1]>') 475 | .add_test('2,(1,0.) -> <[2,1],[2,0]>') 476 | .add_test('(2,3),(1,0.) -> <[2,3,1],[2,3,0]>') 477 | .add_test('\'a,5 -> ["a","5"]') 478 | .add_test('5,\'a -> "5a"') 479 | .add_test('5,"a" -> ["5","a"]') 480 | .add_test('\'b,\'a -> "ba"'), 481 | create_op( 482 | name: "transpose", 483 | sym: "\\", 484 | example: '"abc","123"\\ -> ["a1","b2","c3"]', 485 | type: { [[A]] => [[A]] }, 486 | impl: -> a { transpose(a) }, 487 | ).add_test('"abc","1234"\ -> ["a1","b2","c3","4"]'), 488 | create_op( 489 | name: "reverse", 490 | sym: "/", 491 | example: '"abc" reverse -> "cba"', 492 | type: { [A] => [A] }, 493 | impl: -> a { reverse(a) }, 494 | ), create_op( 495 | name: "unvec", 496 | sym: "%", 497 | example: '1,2+3% -> [4,5]', 498 | type: { VecOf.new(A) => [A] }, 499 | impl: -> a { a.value }, 500 | ), create_op( 501 | name: "vectorize", 502 | sym: ".", 503 | example: '1,2,3. -> <1,2,3>', 504 | type: { [A] => VecOf.new(A) }, 505 | impl: -> a { a.value }, 506 | 507 | # Repl/Debug ops 508 | ), create_op( 509 | name: "type", 510 | example: '1 type -> "Int"', 511 | type: { A => Str }, 512 | no_zip: true, 513 | poly_impl: -> at { -> a { str_to_lazy_list(at.inspect) }}) 514 | .add_test('"hi" type -> "[Char]"') 515 | .add_test('() type -> "[A]"'), 516 | create_op( 517 | name: "version", 518 | type: Str, 519 | impl: -> { str_to_lazy_list("Atlas Alpha (Mar 13, 2023)") } 520 | ), create_op( 521 | name: "reductions", 522 | desc: "operation count so far", 523 | type: Int, 524 | impl: -> { $reductions }), 525 | 526 | # Macros, type only used to specify number of args 527 | create_op( 528 | name: "let", 529 | desc: "save to a variable without consuming it", 530 | example: '5@a+a -> 10', 531 | sym: ApplyModifier, 532 | type: { [A,A] => [A] }, 533 | impl: MacroImpl, 534 | ), create_op( 535 | name: "push", 536 | desc: "duplicate arg onto a lexical stack", 537 | example: '5{,1,},2 -> [5,1,5,2]', 538 | sym: "{", 539 | type: { A => A }, 540 | impl: MacroImpl, 541 | ), create_op( 542 | name: "pop", 543 | desc: "pop last push arg from a lexical stack", 544 | example: '5{,1,},2 -> [5,1,5,2]', 545 | sym: "}", 546 | type: A, 547 | impl: MacroImpl, 548 | ), 549 | 550 | # These are here purely for quickref purposes 551 | create_op( 552 | name: "flip", 553 | sym: "\\", 554 | desc: "reverse order of previous op's args", 555 | example: '2-\\5 -> 3', 556 | type: { :"(a->b->c)" => :"(b->a->c)" }, 557 | impl: MacroImpl, 558 | ), create_op( 559 | name: "apply", 560 | sym: "@", 561 | desc: "increase precedence, apply next op before previous op", 562 | example: '2*3@+4 -> 14', 563 | type: { :"(a->b->c)" => :"(a->b->c)", 564 | :"(a->b)" => :"(a->b)" }, 565 | impl: MacroImpl, 566 | ) 567 | 568 | ] 569 | 570 | Ops0 = {} 571 | Ops1 = {} 572 | Ops2 = {} 573 | AllOps = {} 574 | 575 | def addOp(table,op) 576 | if (existing=table[op.sym]) 577 | combined_type = {} 578 | op.type.each{|s|combined_type[s.orig_key]=s.orig_val} 579 | existing.type.each{|s|combined_type[s.orig_key]=s.orig_val} 580 | combined_impl = -> arg_types,from { 581 | if existing.type.any?{|fn_type| 582 | check_base_elem_constraints(fn_type.specs, arg_types) 583 | } 584 | existing.impl[arg_types,from] 585 | else 586 | op.impl[arg_types,from] 587 | end 588 | } 589 | combined = create_op( 590 | sym: op.sym, 591 | type: combined_type, 592 | final_impl: combined_impl, 593 | ) 594 | table[op.sym] = combined 595 | else 596 | table[op.sym] = op 597 | end 598 | table[op.name] = op 599 | end 600 | 601 | OpsList.each{|op| 602 | next if op.name == "flip" 603 | ops = case op.narg 604 | when 0 605 | addOp(Ops0, op) 606 | when 1 607 | addOp(Ops1, op) 608 | when 2 609 | addOp(Ops2, op) 610 | else; error; end 611 | raise "name conflict #{op.name}" if AllOps.include? op.name 612 | AllOps[op.name] = AllOps[op.sym] = op 613 | } 614 | AllOps[""]=Ops2[""]=Ops2[" "] # allow @ to flip the implicit op (todo pointless for multiplication) 615 | EmptyOp = create_op( 616 | name: "empty", 617 | type: Empty, 618 | impl: []) 619 | UnknownOp = create_op( 620 | name: "unknown", 621 | type: Unknown, 622 | impl_with_loc: -> from { raise AtlasTypeError.new("cannot use value of the unknown type", from) } 623 | ) 624 | Var = Op.new("var") 625 | ToString = create_op( 626 | name: "tostring", 627 | type: { A => Str }, 628 | no_zip: true, 629 | poly_impl: -> t { -> a { to_string(t.type+t.vec_level,a) } } 630 | ) 631 | 632 | def create_int(str) 633 | create_op( 634 | name: "data", 635 | type: Int, 636 | impl: str.to_i 637 | ) 638 | end 639 | 640 | def create_str(str) 641 | raise LexError.new("unterminated string") if str[-1] != '"' || str.size==1 642 | create_op( 643 | name: "data", 644 | type: Str, 645 | impl: str_to_lazy_list(parse_str(str[1...-1])) 646 | ) 647 | 648 | end 649 | def create_char(str) 650 | raise LexError.new("empty char") if str.size < 2 651 | create_op( 652 | name: "data", 653 | type: Char, 654 | impl: parse_char(str[1..-1]).ord 655 | ) 656 | end 657 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Atlas 2 | 3 | Atlas is an [esolang](https://esolangs.org/) that was created to show off the awesome synergy between circular programming and vectorization. It is purely functional but has no functions and little need for them. It has APL like syntax and vectorization without forcing you to use symbols or weird characters. It isn't intended to be good at [code golf](https://en.wikipedia.org/wiki/Code_golf), yet it is actually more succinct than Golfscript and J / K for the average code golf problem - despite its emphasis on simplicity and similar character restraints. 4 | 5 | Currently Atlas is in Alpha. It is very usable and probably has all the important features it will ever have. But it may experience breaking changes and minor improvements. The current interpreter is slow and limited in stack depth (although fine for most toy problems). There is nothing preventing it from being compiled and stackless, it could be as fast as Haskell. 6 | 7 | This page will show off some of the cool things about Atlas and some circular programming tricks that I have not seen anywhere else - which are applicable to other lazy languages like Haskell. For complete information about the language see the [docs folder](docs/). 8 | 9 | You can run code by downloading the Atlas source and saving your Atlas code to a file then running: 10 | 11 | atlas filename.atl 12 | 13 | Or you can use it as a repl with: 14 | 15 | atlas 16 | 17 | You will need Ruby, I have tested it to work with 2.7 and 3.1. 18 | 19 | There is also an [online REPL](https://replit.com/@darrenks/Atlas) available. 20 | 21 | ## Prerequisites to Cool Stuff 22 | 23 | Atlas is like a calculator, all it has are values to which you can apply operators. 24 | 25 | 1+2 26 | ────────────────────────────────── 27 | 3 28 | 29 | Syntax is also like a calculator, left to right. 30 | 31 | 2*3+4 32 | ────────────────────────────────── 33 | 10 34 | 35 | 1-arg operators go on the right. 36 | 37 | 2- 38 | ────────────────────────────────── 39 | -2 40 | 41 | It also has assignment. 42 | 43 | a=5 44 | a*a 45 | ────────────────────────────────── 46 | 25 47 | 48 | Unlike most calculators, it supports lists and strings (which are just a list of characters). 49 | 50 | 1,2,3 51 | "abc" 52 | ────────────────────────────────── 53 | 1 2 3 54 | abc 55 | 56 | Operations that need a lower rank (list depth) for an argument automatically vectorize. Vectors perform operations to all elements. 57 | 58 | 1,2,3 + (3,2,1) 59 | "abc" + 1 60 | ────────────────────────────────── 61 | 4 4 4 62 | bcd 63 | 64 | For operations that are flexible about the rank they apply to such as `head` (which returns the first element of a list) it could be useful to manually vectorize with `.` (or unvectorize with `%`) to adjust the depth the op is applied at. 65 | 66 | a="abc","xyz" 67 | a head 68 | a. head 69 | a.%head 70 | ────────────────────────────────── 71 | abc 72 | ax 73 | abc 74 | 75 | ## Circular Programming Intro 76 | 77 | Let's look at the first example I ever saw of circular programming. It was the Haskell program: 78 | 79 | a = 1 : 1 : zipWith (+) (tail a) a 80 | 81 | Which generates the infinite list of fibonacci numbers. In Atlas this would be: 82 | 83 | a = 1 _ 1 _ (a tail + a) 84 | ────────────────────────────────── 85 | 1 1 2 3 5 8 13 ... 86 | 87 | The only real difference between Atlas and Haskell here is that the zipWith is implicit because Atlas is vectorized. 88 | 89 | The first time I saw this, I just dismissed it as some weird special case trick that I didn't understand and assumed it wasn't practical, but actually it isn't a trick and it is useful, it is probably the most efficient way to compute the Fibonacci numbers sequence in Haskell. 90 | 91 | If we do not want an infinite list, we could just take the first n elements of the list and we can do so without an infinite loop, since we never ask for an infinite number of elements. 92 | 93 | a = 1 _ 1 _ (a tail + a) 94 | a take 10 95 | ────────────────────────────────── 96 | 1 1 2 3 5 8 13 21 34 55 97 | 98 | The `take` syntax is different than Haskell's, in Atlas all functions are treated as operators, since `take` has two arguments it goes between its operands like other binary operators such as `+`. There are symbol versions of all named functions, but this page's goal is to teach concepts and so will not expect you to know them. 99 | 100 | How does it work? First remember that Atlas and Haskell are [lazy](https://en.wikipedia.org/wiki/Lazy_evaluation), so they don't do anything until we ask about the elements of `a`. Atlas starts asking for the elements of `a` in order so that it can print them. It is easy to see that the first 2 elements are 1 without needing to know the rest. The third element is the result of a `zipWith` and is computed only using the first two elements, which we already know. `zipWith` starts moving through these lists as they are generated creating an infinite list. 101 | 102 | If this is explanation of fibonacci numbers is still confusing, here's another way of looking at it. It's like `a` is actually a recursive function that takes no arguments. Consider this definition of a function for `a` in Haskell. 103 | 104 | a() = 1:1:zipWith (+) (a()) (tail(a())) 105 | 106 | It's still relying on laziness to treat the result like a stream, but there is no circular definition of values, only a recursive function definition. 107 | 108 | It is also computes the fibonacci numbers. The base cases work and so does the inductive step so therefore it is correct. But it is exponentially slower, because it computes `a()` twice rather than using the same value. This could be fixed with an assignment: 109 | 110 | a() = let a2 = 1:1:a() in zipWith (+) a2 (tail a2) 111 | 112 | But what is the point of making `a` a function, it doesn't use its argument? Functions without arguments **ARE** values in a pure language! Just define `a` as a value and we end up with the code we started with. 113 | 114 | Laziness will not save the day for all circular programs, only so long as we do not ask for a value in a circle before it has been computed. If we had said `a = a+1` we would hit an infinite loop in Haskell when we ask for the value of `a`. What is `a`? It is `a+1`, and what is `a`? It is `a+1`, etc. Note that this would actually give a type error in Atlas but if you got around that it would raise an infinite loop error since it would also detect the circular dependency at runtime. Circular dependencies of values can always be detected at runtime but not all infinite loops can be since there are other ways to create them (that would break the halting problem). 115 | 116 | Before we go deeper, I'd like to mention that circular programming seems to refer to two different types of things that are related. We will be exclusively talking about the kind you can do by zipping, but there is another type that involves using tuple return values seemingly before they are computed. It is worth knowing about, but not applicable in Atlas. 117 | [Here is a tutorial](http://www.cs.umd.edu/class/spring2019/cmsc388F/assignments/circular-programming.html) on that. 118 | 119 | ### Scanl 120 | 121 | Generating streams that depend on past values of a sequence is probably the most common use of circular programming, but what else can it do? 122 | 123 | We can use it to calculate the scanl of a list and any operation! Suppose we had the list `1,2 3,4` and we wanted to calculate the prefix sums. Of it (e.g. `1,3,6,10`). 124 | 125 | We can do that like this: 126 | 127 | a=1,2,3,4 128 | b=0_b+a 129 | ────────────────────────────────── 130 | 1 3 6 10 131 | 132 | And this works in Haskell too: 133 | 134 | a=[1,2,3,4] 135 | b=zipWith (+) a (0:b) 136 | 137 | It works much the same way as the fibonacci sequence. The first element is the first element of `a` + 0, the second is the result of that + the next element in `a`. 138 | 139 | And we can do a foldl simply be getting the last element of the scanl: 140 | 141 | a=1,2,3,4 142 | b=0_b+a 143 | b last 144 | ────────────────────────────────── 145 | 10 146 | 147 | Having the ability to support functions and just have built-ins for things like scan and foldr would clearly make the language more succinct, but one of the whole reasons for Atlas existence is to force you to learn circular programming and to be extremely simple. 148 | 149 | BTW we can easily generate the list of natural numbers using this technique. 150 | 151 | nats=1_(1+nats) 152 | ────────────────────────────────── 153 | 1 2 3 4 5 ... 154 | 155 | And we can also create repeated numbers: 156 | 157 | ones=1_ones 158 | ────────────────────────────────── 159 | 1 1 1 1 ... 160 | 161 | Or sequences (this is known as also known as tying the knot). 162 | 163 | a=0_b 164 | b=1_a 165 | ────────────────────────────────── 166 | 1 0 1 0 1 0 1 0 ... 167 | 168 | But it could have just been done in one line. 169 | 170 | b = 1 _ 0 _ b 171 | ────────────────────────────────── 172 | 1 0 1 0 1 0 1 0 ... 173 | 174 | ## Foldr 175 | 176 | A foldr is more useful than a foldl because it can lazily terminate. It also doesn't require a function `last` to be written for us in order to achieve it, which is of theoretical interest for creating a small basis for this language. In fact we can implement `last` using a foldr. 177 | 178 | Suppose you wanted to see if any elements in a list were falsey. That is essentially a fold on a logical AND. But with a foldl we can't lazily terminate when we find one. However a foldr is like getting the first element of a scanr and will lazily terminate as soon as one is found. 179 | 180 | When first exploring the ideas of this language I didn't think it was possible to scanr or foldr with just basic head/tail ops. I was really blown away when I discovered how to. It can actually be done in much the same way as scanl, but with a bit of extra care. 181 | 182 | To sum via scanr we want the last element to be last element of the original list + 0, and the 2nd to last to be the sum of the last 2 numbers + 0 and so on. So in Haskell it seems we could just write the Haskell code: 183 | 184 | a = [1,2,3,4] 185 | b = zipWith (+) a (tail (b++[0])) 186 | 187 | Another way to think of it is the first element is the sum of the first list element and the sum of a recursive step. 188 | 189 | But this causes an infinite loop. Why? 190 | 191 | The reason is because zipWith stops when either list is empty. And to decide if `b` is empty it needs to know if the tail of `b++[0]` is empty. But to find where the `[0]` begins it needs to know if `b` is empty, which is where we started. 192 | 193 | There are ways to get around this issue in Haskell, see the [circular programming](docs/circular.md) section for more details. 194 | 195 | In Atlas this isn't an issue however, because it assumes that vector operations will be non empty until proven otherwise. Essentially it finds the "greatest fixed point" if it exists whereas Haskell always returns the "least fixed point." So we can just write. 196 | 197 | a = 1,2,3,4 198 | b = b,0 tail+a 199 | ────────────────────────────────── 200 | 10 9 7 4 201 | 202 | Taking the head of this list is a truly lazy foldr. 203 | 204 | ## Minimal Basis of Turing Complete Ops 205 | 206 | An impressive thing about the lambda calculus is that just lambda or even just the S and K combinators are sufficient to be Turing complete. How simple could Atlas be? 207 | 208 | Folds are extremely powerful (see [expressiveness of fold](https://www.cs.nott.ac.uk/~pszgmh/fold.pdf)). My take is that we can think of any computation as repeatedly generating a new state based solely on a previous state. I guess this is more of an `iterate` in Haskell terms. But iterate is just a fold on an infinite list where you don't use the arg. 209 | 210 | Since we have seen how to foldr, we can use it to get the nth state or last state. Just foldr, taking the first element where index=n. This would need something that resembles an if statement. 211 | 212 | To create an if statement of the form `if a then b else c`, we could treat a non-empty list as true and an empty list as false. Then if we replace all elements of the list `a` with the value `b`, and finally append `c` then just taking the first element of the result would accomplish the task, because if `a` was false (empty) then the value would be `c` but if `a` was true (non empty), it would be the first element which has been replaced with `b`. 213 | 214 | We have ops for append and head but what about replace? That can be done by doing a vectorized no op. One way to do that is to append two elements and take the head, thus ignoring the second value. 215 | 216 | a=() 217 | b="a is true" 218 | c="a is false" 219 | b;.,(a.)%,c head 220 | a=(); 221 | b;.,(a.)%,c head 222 | ────────────────────────────────── 223 | a is false 224 | a is true 225 | 226 | This code uses `;` (`single`) which just creates a list of one element. It also uses `()` which creates a list of zero elements of unknown (any) type. 227 | 228 | Lambda calculus uses Church Numerals to represent numbers. We can just use lists to represent numbers, where the length of the list is the number. So we don't need actual numbers. But what are our lists lists of then? `()` creates a list of unknown type which would suffice. But that's kind of a special case that `()` means that, so how could we create atoms without it? Circular programs to the rescue again. 229 | 230 | a=a 231 | ────────────────────────────────── 232 | 1:3 (a) cannot use value of the unknown type (TypeError) 233 | 234 | It is an error to access the value of `a`, but we can create lists of them and not access them. 235 | 236 | a=a 237 | a;,a,a len 238 | ────────────────────────────────── 239 | 3 240 | 241 | `,` is the same as appending a single value, so it can be made with `_` and `;`. 242 | 243 | So we need the following ops `.` `%` `=` `;` `head` `tail` `_`. 244 | 245 | And all of these are very trivial (O(1)) ops except for `_`. But Atlas could have been made where lists are created symmetrically using a tree structure rather than a cons structure, in which case this would make that operation trivial as well, although then head and tail would be more complicated. 246 | 247 | `_` and `;` could be combined into one op. If you wanted `;` just apply the combined op with an empty list, and if you wanted `_` just take the head of the result. This brings us down to 6 ops. 248 | 249 | I could argue that `%` and `.` aren't really ops. They are indeed actually just no-ops that alter the return type. It might be possible to rely on auto vectorization/unvectorization to do away with them visually even though the generated intermediate representation would still be the same. FYI the whole concept of vector types isn't needed, and instead we could just provide a vectorized version of each op that we wish to use. Or maybe we don't need the unvectorized versions of the ops and could have only the vectorized versions? 250 | 251 | `=` also isn't really an op, it actually just connects things in the intermediate representation which is just a graph of ops and their arguments, it's requirement is purely an artifact of having to write our code (which is ultimately a graph) as 1d text. And if we are going to go that far, we might as well combine head and tail into one op called `uncons` which returns two things. If we are speaking graph-wise then that would be no problem. So Atlas only needs 2 ops (`uncons` and `appendSingle`) and they are both simple? Debatable. 252 | 253 | Regardless of how many ops you consider the minimal basis to be it is worth noting that in some ways it is simpler than the lambda calculus. The lambda calculus is untyped and would lose its Turing completeness if it were to be statically typed. However Atlas is easily statically typed, in fact you may not have realized it because you never need to specify type but the implementation actually is statically typed! Also lambdas are more difficult to implement lazily than values and have a lot going on behind the scenes with substitutions. Atlas' graph intermediate representation is definitely simpler than the lambda calculus'. 254 | 255 | Lambda is of course very useful and beautiful, but I find it a nice result we don't even need it, so long as we have access to vectorized versions of head, tail, append, and single. 256 | 257 | ## Complex State 258 | 259 | I've mentioned how you can think of any computation as a sequence of states and therefore accomplish it with circular programming. But said states are surely complicated right? And here we have only been using lists of scalars, there aren't even tuples or ways of constructing new types in Atlas. We don't need them though. If you want a list of tuples, just use two lists. Anytime you need to do an operation involving both values of the tuple, just use your operation on the two lists (each vectorized) and they will zipWith. If you have a tuple of size 3 and want to do an operation that needs all 3, it would actually be two operations (assuming it is a binary op), and so you would just be doing two separate zipped operations on 3 lists. 260 | 261 | This way of handling complex state isn't as contrived as it sounds. For example Haskell has functions like mapAccumL, unfoldr, iterate, and scanl which can get tedious to use if you need to use tuples in your state - typically people switch to using monads in these cases. But these functions are really a single concept that is more elegantly expressed with a circular program. 262 | 263 | For example consider our original Fibonacci numbers program, with iterate that would have been: 264 | 265 | map fst $ iterate (\(a,b)->(b,a+b)) (1,1) 266 | 267 | Compared to Atlas 268 | 269 | a=1_1_(a tail+a) 270 | ────────────────────────────────── 271 | 1 1 2 3 5 8 13 ... 272 | 273 | And that's still a pretty simple case, you can combine any number of values in any way. The larger the tuples, the more elegantly circular programming will express the computation - albeit it may be harder to reason about to the untrained eye (or possibly all eyes?). 274 | 275 | 276 | *the rest of this intro is an unfinished state* 277 | 278 | ----------------------------------------- 279 | 280 | 281 | TODO more complex example but more readable than my bf interpreter 282 | 283 | Just in case there was any doubt that the language is Turing Complete, I'll use these principles to implement a brianfuck interpreter: 284 | 285 | '\0+("++++++++++[>+++++++>++++++++++>+++>+<<<<-]>++.>+.+++++++..+++.>++.<<+++++++++++++++.>.+++.------.--------.>+.>.":v2](0‿(0,‿(v5[(0‿(v1=='> then v6+1 else (v1=='< then v6-1 else v6)):v6)!@(v1=='+ then v4+1 else (v1=='- then v4-1 else v4)!;!@(v5](v6+1)))):v5]v6![:v4 then 0 else 1*(v1=='[!#) then v3+(1+(0‿(v2=='[ then v8+1 else (v2=='] then v8-1 else v8)):v8]v3![:v7!,!!==(v8](v3+1)) then 0‿(v9+(1,)):v9!;, else ($,,)!_![)) else (v4!&(v1==']) then v7-1!,!!==(v8[v3) then v9!;, else ($,,)!_!] else (v3+1))):v3)![:v1=='. then v4!; else ($,)_) 286 | 287 | 288 | source="++++++++++[>+++++++>++++++++++>+++>+<<<<-]>++.>+.+++++++..+++.>++.<<+++++++++++++++.>.+++.------.--------.>+.>." 289 | bracket_depth = 0 !if source == '[ then bracket_depth+1 else !if source == '] then bracket_depth-1 else bracket_depth 290 | not_truthy = !if value then 0 else 1 291 | wholes=0 wholes+,1 292 | code_pointer = 0 !if not_truthy * !len instruction == '[ then find_rbracket else !if value !& instruction == '] then find_lbracket else code_pointer+1 293 | instruction = !head code_pointer drop source 294 | pointer = 0 !if instruction == '> then pointer+1 else !if instruction == '< then pointer-1 else pointer 295 | state = (,0) (pointer take state) !@ (!;new_value) !@ (pointer+1) drop state 296 | 297 | value = !head pointer drop state 298 | new_value = !if instruction == '+ then value+1 else !if instruction == '- then value-1 else value 299 | 300 | current_bracket_depth = !head code_pointer drop bracket_depth 301 | 302 | // first point where bracket_depth = bracket_depth again 303 | find_rbracket = code_pointer + 1 + !head !concat !!if (!,current_bracket_depth) !!== (code_pointer+1) drop bracket_depth then ,!;wholes else ,,$ 304 | 305 | // last point where bracket_depth = bracket_depth again 306 | find_lbracket = !last !concat !!if (!,current_bracket_depth-1) !!== code_pointer take bracket_depth then ,!;wholes else ,,$ 307 | 308 | output = !if instruction == '. then !;value else ,$ 309 | 310 | // todo terminate when code_pointer > source size 311 | '\0+concat output 312 | 313 | --------------------------- 314 | Hello World! 315 | 316 | 6:15 (!head) head on empty list (DynamicError) 317 | 318 | TODO update this code to the new left to right syntax 319 | 320 | It can automatically be minified to: 321 | 322 | s="++++++++++[>+++++++>++++++++++>+++>+<<<<-]>++.>+.+++++++..+++.>++.<<+++++++++++++++.>.+++.------.--------.>+.>." 323 | '\0+_((a=![((b=0:(((c=![((d=0:(a='>)!?d+1)(a='<)!?d-1)d)}e=(,0):(d{e)!@(!;((a='+)!?c+1)(a='-)!?c-1)c))!@(d+1)}e))!?0)1)*a='[)!?b+1+![!_((!,(f=![(b}g=0:((s)='[)!?g+1)(s='])!?g-1)g)))!!=(b+1)}g)!!?,!;h=0:h+,1),,$)((c!?1)0)*a='])!?!]!_((!,(f-1))!!=b{g)!!?,!;h),,$)b+1)}s))='.)!?!;c),$ 324 | 325 | Note that you could use `I` for input rather than hard coding the input as `s=...`. 326 | 327 | TODO regen minified given some new syntax changes. 328 | 329 | That's a brainfuck interpreter (without input, but it could be supported similarly) in about 281 characters using only a handful of built in operations in a purely functional language. The brainfuck code might be more readable though... This could be done better, it is only my first attempt, and I'm new to writing complex problems in Atlas too! Some downsides to my method are that I treat lists like arrays and do random access using head of a drop, this is slow and verbose. Concepts like zippered lists could greatly improve it. 330 | 331 | 332 | ## Final thoughts on Circular Programming and Vectorization 333 | 334 | Circular programming is a powerful technique, especially when your language has a nice syntax for vectorization. These techniques are often the simplest and best way to describe a sequence, but as I've shown you can take it too far. Moderation is key for real code. Still, going too far in play is enlightening, and hopefully you can have some fun playing with Atlas. 335 | 336 | Let's end with two examples where circular programming is an elegant solution. The first "real world" problem is the Josephus Problem. Where 40 prisoners decide to go around in a circle, every third prisoner shooting themselves, until only 1 is left, which spot should you line up at to survive? The [haskell solution](https://rosettacode.org/wiki/Josephus_problem#Haskell) is 39 lines long without circular programming and [16 with](https://debasishg.blogspot.com/2009/02/learning-haskell-solving-josephus.html). There is a 1-line without circular programming but it is non obvious. Implementing it intuitively is simpler with circular programming. 337 | 338 | In Atlas the solution is simple: 339 | 340 | nats=nats+1`1 341 | prisoners=nats take 40 342 | gunHolders = prisoners append (nats % 3. and (gunHolders.;)% concat) 343 | gunHolders tail.=gunHolders concat head 344 | ────────────────────────────────── 345 | 28 346 | 347 | The `!then gunHolders!; else $ concat` which is used twice may look scary but that's actually just a common pattern for doing a filter, not a fault of circular programming that Atlas lacks that operator for now. That last line is just selecting the first case it is the same person twice in a row. That catch op I alluded to earlier would be really handy here then we could just take the last before encountering an error. 348 | 349 | I guess it is no surprise that a problem involving a circle has a nice circular programming solution. But calculating primes using the Sieve of Eratosthenesis is our next example. Typically the sieve is done on a fixed size, but if you use circular programming you can stream it. 350 | 351 | There is a [functional pearl article](https://arxiv.org/pdf/1811.09840.pdf) about an even more efficient solution and it too uses circular programming. 352 | 353 | TODO write alg from that article in Atlas 354 | 355 | Now that I have some experience it would seem contrived to NOT use circular programming for solving these problems in any language. 356 | 357 | # Future / Contributing 358 | 359 | It could be worth porting Atlas to a more efficient language than Ruby, or maybe support compiling Atlas to C or Haskell for faster execution. It would be trivial to compile to Haskell except for the greatest fixed point capabilities to avoid infinite loops - I'm not sure how you would do that in Haskell. 360 | 361 | It could be worth creating a binary version of the language where each instruction is 5 bits for a 38% code size reduction - which would be easy since Atlas uses only the 32 symbols (variables and numbers can be fit in too since Atlas doesn't actually use all symbols for both binary and unary operations yet). However it is a little silly to compete between different languages and it probably wouldn't be shorter than [Nibbles](http://www.golfscript.com/nibbles/) for most programs since it is far simpler. 362 | 363 | Bug reports, design thoughts, op ideas, painpoints, feedback, etc. would be greatly appreciated. Please raise an issue or email me with your thoughts. ` at golfscript.com`. 364 | 365 | There is also a google group for discussing the ongoing design of the language: [atlas-lang](https://groups.google.com/g/atlas-lang). Please ask to join with a message (this is just to prevent spam, all are welcome). 366 | 367 | BTW there was a working Befunge-like 2D mode where you draw your program's graph rather than use variable names and parenthesis - it was smart about deducing connections so was probably even more concise. It was very interesting, but I removed it for now to focus on teaching circular programming rather than exploring a wacky idea (check out the very first commit of this language for a working prototype if you are curious). 368 | --------------------------------------------------------------------------------