Compare commits

...

571 Commits
main ... v0.1.0

Author SHA1 Message Date
Sasha Koshka 89311688a4 Inline string literal since #46 has been remedied 2024-03-01 00:17:39 -05:00
Sasha Koshka a4ddab7bb4 TestLiteralReference is now an error test 2024-03-01 00:13:32 -05:00
Sasha Koshka 4aa0a0f304 Fixed analyzer.isLocationExpression 2024-03-01 00:12:21 -05:00
Sasha Koshka 71a1f0a253 Add test case for #46 2024-03-01 00:04:15 -05:00
Sasha Koshka bd2172b732 Merge pull request 'generator-multi-unit-tests' (#51) from generator-multi-unit-tests into main
Reviewed-on: #51
2024-03-01 05:00:07 +00:00
Sasha Koshka e2cb3820cf Add some multiunit tests from the analyzer to the generator 2024-02-29 23:59:15 -05:00
Sasha Koshka eac7d492d6 Change name of generator multiunit test to match analyzer 2024-02-29 23:52:09 -05:00
Sasha Koshka acf8046114 Putting complete covering of bit casts on hold for now 2024-02-28 20:08:54 -05:00
Sasha Koshka 69c4294522 Covered more cases with bitcast 2024-02-28 20:08:43 -05:00
Sasha Koshka 7acc9c5ab0 Add more test cases for bitcasting pointers 2024-02-28 19:49:35 -05:00
Sasha Koshka f92008046b Merge branch 'main' into generator-multi-unit-tests 2024-02-28 19:42:11 -05:00
Sasha Koshka f7b590d823 Merge pull request 'analyzer-fix-bitcast' (#50) from analyzer-fix-bitcast into main
Reviewed-on: #50
2024-02-29 00:34:41 +00:00
Sasha Koshka c2cdb86b12 The buge (#47) is vanquished!!!! 2024-02-28 19:30:33 -05:00
Sasha Koshka 7225ef7dc3 Expressions now have HasExplicitType() 2024-02-28 18:31:46 -05:00
Sasha Koshka e24d22a24f Add test case to analyzer for #47 2024-02-28 18:03:15 -05:00
Sasha Koshka f3e9f33dd3 Completed out TestUnitWriterInterface 2024-02-28 17:43:17 -05:00
Sasha Koshka aa82eb190a Renamed TestPtrIntCast to TestPtrCast, will put more things in it 2024-02-28 17:36:11 -05:00
Sasha Koshka 35479e68a8 Add and fix test case for #47 (on the generator side at least) 2024-02-28 13:36:21 -05:00
Sasha Koshka 40c5d10d4b Generator uses proper type owner UUID during interface conversion 2024-02-28 12:30:38 -05:00
Sasha Koshka 68be34ddfe Generator multiunit tests print what is being analyzed 2024-02-28 11:50:41 -05:00
Sasha Koshka ba58259eaf Fix example unit UUID 2024-02-27 19:50:55 +00:00
Sasha Koshka e2a61e9506 Minor grammar fixes 2024-02-27 19:41:19 +00:00
Sasha Koshka 0395df9944 Fix ordering of dirs 2024-02-27 19:32:23 +00:00
Sasha Koshka 774f36724d Correct dir paths 2024-02-27 19:30:54 +00:00
Sasha Koshka 1c2186e9fc Add problematic multiunit test case from #49 2024-02-27 03:06:54 -05:00
Sasha Koshka f21828794e Merge pull request 'test-compiled-code' (#48) from test-compiled-code into main
Reviewed-on: #48
2024-02-27 07:50:17 +00:00
Sasha Koshka 5e740ebf5f Compiler has bug() to wrap unexpected errors 2024-02-27 02:45:32 -05:00
Sasha Koshka d5bb1be894 Add some interface tests to compiler 2024-02-27 02:36:35 -05:00
Sasha Koshka 0c39d0a1cb Compiler debug output makes more sense 2024-02-27 02:33:35 -05:00
Sasha Koshka dda8cea996 Generator's "errNotFound" is more useful when it bubbles to the surface 2024-02-27 02:29:52 -05:00
Sasha Koshka 2c3bc9acce Fix analyzer.Tree.methodExists and clear raw maps
This was too urgent not to continue without
2024-02-27 02:24:03 -05:00
Sasha Koshka 60c49a1d9a Add test for printing argv
Disabled for now due to #47
2024-02-27 00:22:02 -05:00
Sasha Koshka d92aa7c3a9 Add test case for counting cli args 2024-02-27 00:09:01 -05:00
Sasha Koshka 73f70304ed Temporarily circumvent crash in analyzer #46 2024-02-27 00:00:33 -05:00
Sasha Koshka 2b9c99fff2 Added a test case for depending on compiled units 2024-02-26 23:59:47 -05:00
Sasha Koshka 4c26410274 Added a test case for depending on compiled units 2024-02-26 23:50:14 -05:00
Sasha Koshka cc2b1315a0 Tests can now compile dependencies first 2024-02-26 23:32:40 -05:00
Sasha Koshka 785a77b21b Fixed test case file for systeminclude 2024-02-26 14:40:03 -05:00
Sasha Koshka cc3315280a Resolver no longer requires system units to be files (????????) 2024-02-26 14:39:44 -05:00
Sasha Koshka ad828ee01e Added test case for grabbing a module from /usr/include/fspl 2024-02-26 14:33:31 -05:00
Sasha Koshka 179c0e00c2 Fix hello world test 2024-02-26 14:24:47 -05:00
Sasha Koshka c1d574b60f Compiler test now uses testcommon.CompareHex 2024-02-26 14:22:51 -05:00
Sasha Koshka 6bef8aea76 Add testcommon.CompareHex 2024-02-26 14:22:33 -05:00
Sasha Koshka 818b8058e4 testcommon.LogColumns has a configurable width 2024-02-26 14:18:09 -05:00
Sasha Koshka e5144fdf6a Split compiler_test.go into common file and test file 2024-02-26 14:04:37 -05:00
Sasha Koshka 5c8afd52af Added test case for exit code 2024-02-26 14:03:10 -05:00
Sasha Koshka e87717ac21 Compiler test now holds output from compiler until end 2024-02-26 13:56:37 -05:00
Sasha Koshka 9459f6192b Compiler uses a default optimization level when unconfigured 2024-02-26 13:46:54 -05:00
Sasha Koshka c65a307f4a Add the hello world test case 2024-02-26 13:46:30 -05:00
Sasha Koshka da434f00b6 Add a test-data directory for compiler 2024-02-26 13:46:10 -05:00
Sasha Koshka f42b76d5c1 Compiler now passes relatvive paths to fs.FS as it should 2024-02-26 12:50:49 -05:00
Sasha Koshka 33995288cc Much of the compiler now uses fs.FS instead of os 2024-02-26 11:46:59 -05:00
Sasha Koshka a158f26120 Moved everything that should touch "os" to compiler.go 2024-02-26 11:36:16 -05:00
Sasha Koshka 515eb172a5 Moved Compiler.CompileUnit into another file
Need to be sure it is the only thing that can access OS files
2024-02-26 11:35:03 -05:00
Sasha Koshka e55e332e17 Fixed doc for fsplc and fsplmod 2024-02-23 03:20:51 -05:00
Sasha Koshka 2fc272fccf Add a tool to manage modules 2024-02-23 01:41:36 -05:00
Sasha Koshka 7ad4bec332 Lexer skips over zero runes now 2024-02-23 01:08:58 -05:00
Sasha Koshka 0bbeeda319 Metadata.String quotes UUID 2024-02-23 00:24:09 -05:00
Sasha Koshka 3db6aa211e entity.Metadata now has String() 2024-02-23 00:18:46 -05:00
Sasha Koshka a0f29f36f5 Cli now handles printing stuff 2024-02-22 21:24:07 -05:00
Sasha Koshka 779c50e961 Cli now displays usage information for subcommands correctly 2024-02-22 20:25:37 -05:00
Sasha Koshka 7e98950574 Update fsplc to use new cli features 2024-02-22 20:17:38 -05:00
Sasha Koshka ea20d1d022 Update fsplc doc 2024-02-22 20:17:23 -05:00
Sasha Koshka 2a622a1db3 Add more functionality to cli package 2024-02-22 20:12:41 -05:00
Sasha Koshka 623bcabb84 Changed repository import paths 2024-02-22 19:22:53 -05:00
Sasha Koshka 7b9b45e5ac Changed go.mod version to 1.19 2024-02-21 13:49:46 -05:00
Sasha Koshka 4c6ff81c18 Reflected those changes in unit design doc 2024-02-21 13:46:26 -05:00
Sasha Koshka 7f31fc1547 Module search paths are more useful now 2024-02-21 13:45:51 -05:00
Sasha Koshka a580d5d796 Merge branch 'implement-modules' 2024-02-21 13:27:35 -05:00
Sasha Koshka 7f4e23fb71 Merge branch 'main' into implement-modules 2024-02-21 18:26:44 +00:00
Sasha Koshka 7d530772bf Updated compiler to use Resolver 2024-02-21 13:21:58 -05:00
Sasha Koshka 24a7995819 Add a "Resolver" that resolves unit addresses 2024-02-21 13:21:41 -05:00
Sasha Koshka 1ba6afef23 Compiler understands compiling to . 2024-02-20 01:48:18 -05:00
Sasha Koshka bfee5ecc92 Compiler now only parses module files that end in .fspl 2024-02-20 01:39:07 -05:00
Sasha Koshka c49e963310 cli package now prints out usage correctly 2024-02-20 01:27:26 -05:00
Sasha Koshka 6f429571ff Compiler is a bit more adaptable now 2024-02-20 01:23:00 -05:00
Sasha Koshka 3ef8ecca66 Oops haha, print statement. 2024-02-20 00:52:30 -05:00
Sasha Koshka 2cedeb9c0d The compiler now compiles 2024-02-20 00:52:23 -05:00
Sasha Koshka 12bf91d010 Updated generator tests 2024-02-19 23:05:30 -05:00
Sasha Koshka 49b6264ba4 Fixed generator map sorting 2024-02-19 21:57:21 -05:00
Sasha Koshka c4873915bb Types are given proper link names in the generator 2024-02-19 21:09:21 -05:00
Sasha Koshka 66ee370f23 Generator now has module support 2024-02-19 21:00:20 -05:00
Sasha Koshka 34c6fcbd57 Documented how methods are named 2024-02-19 12:09:21 -05:00
Sasha Koshka b6edaa0814 entity.Key now has an optional method field 2024-02-19 12:05:13 -05:00
Sasha Koshka 33b6414647 Move Key to Entity 2024-02-19 11:41:36 -05:00
Sasha Koshka abb326bffc Fixed wording issue in design/units.md 2024-02-19 11:18:17 -05:00
Sasha Koshka 74c6d93151 Analyzer does not allow operations on restricted types 2024-02-17 00:19:13 -05:00
Sasha Koshka dd09b86cb8 Analyzer does not allow literals to be assigned to restricted types 2024-02-17 00:12:32 -05:00
Sasha Koshka f5bae2c231 Analyzer does not allow value casting of restricted types 2024-02-16 23:52:52 -05:00
Sasha Koshka f0d42f996c Analyzer does not allow subscripting, slicing of restricted types 2024-02-16 23:10:38 -05:00
Sasha Koshka a9522b5c20 Add more multi-unit test cases 2024-02-16 22:50:29 -05:00
Sasha Koshka 3039bdbaf7 Analyzer does not allow marking functions/methods as restricted 2024-02-16 22:31:38 -05:00
Sasha Koshka 6ea10c3783 Analyzer does not allow member access on restricted types 2024-02-16 22:26:22 -05:00
Sasha Koshka ac56be4468 Analyzer does not allow calling behaviors of restricted iface 2024-02-16 22:19:47 -05:00
Sasha Koshka e0cb31a85b Analyzer no longer thinks String is private 2024-02-16 22:13:30 -05:00
Sasha Koshka ef2a2ab059 Analyzer now fills out this information 2024-02-16 13:51:11 -05:00
Sasha Koshka 1c6e179f0a Entity now stores unit and access information for types 2024-02-16 13:30:15 -05:00
Sasha Koshka 3ef9f404b1 Should also make sure ~ only applies to types 2024-02-16 13:09:48 -05:00
Sasha Koshka e38c185042 Add more test cases for restricted typedefs in units 2024-02-16 13:06:40 -05:00
Sasha Koshka 745e79157c When analyzing a typedef, update the unit information first 2024-02-16 12:54:34 -05:00
Sasha Koshka f6eeb011bf This is mega silly 2024-02-16 12:49:47 -05:00
Sasha Koshka 95db63b1b8 Pass TestUnitPrivateMethod Err 2024-02-16 12:48:25 -05:00
Sasha Koshka d2cb1f78c4 Fix behavior of Tree.analyzeMethodOrBehavior() 2024-02-16 12:43:42 -05:00
Sasha Koshka 6dc958742b Real quick add unicode test to parser 2024-02-15 12:43:09 -05:00
Sasha Koshka 0684410631 Ok so maybe that one commit wasn't entirely true 2024-02-15 01:12:51 -05:00
Sasha Koshka e2367b26c2 Analyzer checks access permissions when using named types 2024-02-15 01:08:21 -05:00
Sasha Koshka 8019a11826 Parser properly unions position of named types 2024-02-15 01:07:25 -05:00
Sasha Koshka 9178e55a20 Parser does *not* actually discard private toplevels while skimming
And it shouldn't! Because it doesn't, the analyzer can say "this is
private" instead of "this doesn't exist".
2024-02-15 01:06:21 -05:00
Sasha Koshka 78fa0f703d Add some initial analyzer tests for units 2024-02-14 23:20:01 -05:00
Sasha Koshka afe3683c2a I ohasdhjklashf dslkfj ksdl 2024-02-14 23:10:43 -05:00
Sasha Koshka 699ef84bef Forgot about the nickname map haha 2024-02-14 22:48:55 -05:00
Sasha Koshka 75d89e9c2a Should be able to write tests for units now.
Haha unit tests. Surely this won't get confusing.
2024-02-14 22:39:08 -05:00
Sasha Koshka 49493fe7d0 Improved analyzer testing infrastructure 2024-02-14 22:06:10 -05:00
Sasha Koshka 7a4b9850df Parser now actually skims files 2024-02-14 18:49:59 -05:00
Sasha Koshka 5940d47ad7 Improved the skim test case 2024-02-14 17:40:37 -05:00
Sasha Koshka 736f117fac Add parser test case for skimming 2024-02-14 17:35:44 -05:00
Sasha Koshka ee4dfc3e4a All analyzer tests pass 2024-02-14 17:24:00 -05:00
Sasha Koshka 7420fac2bd I mispelled stream 2024-02-14 13:49:06 -05:00
Sasha Koshka bc5f435ea6 Made analyzer error testing 20% cooler 2024-02-14 13:48:23 -05:00
Sasha Koshka 06b1cc0cb6 Use stram0.fspl so I don't have to rewrite every test 2024-02-14 13:41:24 -05:00
Sasha Koshka 953e9336db Analyzer does not crash 2024-02-14 13:40:26 -05:00
Sasha Koshka 04a94a2dc9 Analyzer compiles 2024-02-14 13:35:50 -05:00
Sasha Koshka ab1f25785b Add section about uniqueness and UUIDs to units design doc 2024-02-14 13:00:11 -05:00
Sasha Koshka 940346eecf Begin implemeting my better idea in the analyzer 2024-02-14 10:38:03 -05:00
Sasha Koshka 832e9232aa Made wording in entity even better 2024-02-14 10:22:33 -05:00
Sasha Koshka 2985d13131 Add a file with my better idea in it 2024-02-14 10:22:08 -05:00
Sasha Koshka f30e6d839d So I have a better idea 2024-02-14 10:06:20 -05:00
Sasha Koshka b27d5fe006 treeParser.lookupUnit returns the current unit UUID on empty string 2024-02-14 02:04:28 -05:00
Sasha Koshka 98789cf779 Parser and entity now refer to units as units instead of modules 2024-02-14 02:01:01 -05:00
Sasha Koshka 8ad421b1e1 Parser now translates names to unit UUIDs 2024-02-14 01:55:51 -05:00
Sasha Koshka 794d44787d Entity now deals in UUIDs 2024-02-14 01:48:47 -05:00
Sasha Koshka c69091898a Parser now has something of a concept of modules 2024-02-14 01:44:25 -05:00
Sasha Koshka 89500ec5ef Add module names to top level entities 2024-02-13 21:36:08 -05:00
Sasha Koshka 1636fa0172 Mini-analysis of metadata in Compiler.ParseModule 2024-02-13 21:24:13 -05:00
Sasha Koshka a46939b3f8 Break compiler out into its own package 2024-02-13 21:01:13 -05:00
Sasha Koshka 87fa994f27 Break out ParseModule and ParseSourceFile from CompileUnit 2024-02-13 20:55:59 -05:00
Sasha Koshka a4b60a5eff Metadata is now an entity 2024-02-13 20:53:15 -05:00
Sasha Koshka a9d470d46a Metadata is now an entity 2024-02-13 20:52:25 -05:00
Sasha Koshka 50cbf25c1e Compiler CLI can drive internals 2024-02-13 19:15:40 -05:00
Sasha Koshka 0bcd41c873 Module compilation stub in compiler command 2024-02-13 19:10:46 -05:00
Sasha Koshka a240e26776 Add method of entity.Address to generate a nickname 2024-02-13 19:10:24 -05:00
Sasha Koshka 81bef17d6f Add some utility methods to entity.Address 2024-02-13 17:29:42 -05:00
Sasha Koshka fb374c5420 Auto-nicknaming should happen at the analysis stage 2024-02-13 17:08:48 -05:00
Sasha Koshka 87d5ca4cee Add test cases for metadata parser 2024-02-13 16:33:15 -05:00
Sasha Koshka a837577217 Add metaParser test-common.go 2024-02-13 16:32:59 -05:00
Sasha Koshka 211a6d358a Implemented metadata file parsing 2024-02-13 16:22:10 -05:00
Sasha Koshka 9598890aa4 Changed wording in fsplParser.treeParser.bug() 2024-02-13 16:21:50 -05:00
Sasha Koshka b4adef7a1c Nickname is no longer a type 2024-02-13 16:20:45 -05:00
Sasha Koshka 24961269f2 Flesh out meta.Tree and meta.treeParser with more methods 2024-02-13 16:03:22 -05:00
Sasha Koshka c68b3ab4c1 Add Directive interface 2024-02-13 16:03:07 -05:00
Sasha Koshka bda19ccdcf Minor readme fixes 2024-02-13 19:58:28 +00:00
Sasha Koshka f06ca0250a Add information (or lack thereof) of learning the language 2024-02-13 19:57:22 +00:00
Sasha Koshka b184056a9d Update roadmap 2024-02-13 19:40:51 +00:00
Sasha Koshka f3df9ce15a License under GPLv3 2024-02-13 19:16:11 +00:00
Sasha Koshka 364141ad0d Add metadata entities 2024-02-13 13:29:30 -05:00
Sasha Koshka 27947f7ca4 Updated compiler command 2024-02-13 13:16:03 -05:00
Sasha Koshka 6bc5cc8766 I blame debian for this 2024-02-13 13:14:45 -05:00
Sasha Koshka b6bd81a244 Updated generator 2024-02-13 13:12:53 -05:00
Sasha Koshka ce6ca856ee Updated analyzer 2024-02-13 13:12:45 -05:00
Sasha Koshka 6bb5846427 Improve doc comments for Parser 2024-02-13 13:07:37 -05:00
Sasha Koshka 548c8c4de1 Separated parser into two packages 2024-02-13 13:03:22 -05:00
Sasha Koshka 7093e65aed Extracted re-usable parsing utilities from parser.parser 2024-02-13 12:31:52 -05:00
Sasha Koshka 76d71cd1e8 Asajkdhaskldj 2024-02-13 00:50:30 -05:00
Sasha Koshka 64e5adcf41 Rename module design doc to unit design doc 2024-02-13 00:27:51 -05:00
Sasha Koshka 4385b4cf75 Add future work section 2024-02-13 00:10:14 -05:00
Sasha Koshka c76711cd02 Revised module design, cleared up wording 2024-02-13 00:01:39 -05:00
Sasha Koshka 66d618a6fc Add design document for modules 2024-02-12 23:37:04 -05:00
Sasha Koshka 2c127c5170 Merge pull request 'fix-nil-void-type' (#36) from fix-nil-void-type into main
Reviewed-on: sashakoshka/fspl#36
2024-02-12 19:49:57 +00:00
Sasha Koshka 83b58c1ef6 Analyzer makes use of entity.FormatType 2024-02-12 14:47:49 -05:00
Sasha Koshka f77f341c3d Add test cases for formatting nil type as Void 2024-02-12 14:47:09 -05:00
Sasha Koshka 3662bda3a8 Add entity.FormatType() 2024-02-12 14:34:49 -05:00
Sasha Koshka f2f522645f Merge pull request 'polish-compiler' (#35) from polish-compiler into main
Reviewed-on: sashakoshka/fspl#35
2024-02-11 18:06:08 +00:00
Sasha Koshka eb7a34115b Compiler now takes in a format override parameter 2024-02-11 12:55:43 -05:00
Sasha Koshka 58a15d3da0 Add cli.NewValueSet() to validate against several values 2024-02-11 12:38:46 -05:00
Sasha Koshka 928bf64d9d Fix some issues with cli related to argument values 2024-02-11 12:37:08 -05:00
Sasha Koshka 385657a5c1 Better documentation for cli package 2024-02-11 12:32:53 -05:00
Sasha Koshka f6b25c8543 Broke cli into a separate package 2024-02-11 12:13:49 -05:00
Sasha Koshka f90364697c Compiler now parses CLI argument values correctly 2024-02-11 03:40:47 -05:00
Sasha Koshka 8bf97af763 Compiler prints a special error if output path has no extension 2024-02-11 03:40:00 -05:00
Sasha Koshka 20ed736823 errors.Format does not crash when formatting normal error 2024-02-11 03:35:27 -05:00
Sasha Koshka a68633f061 Created a new argument parsing system 2024-02-11 03:33:16 -05:00
Sasha Koshka ac16322860 Break fsplc Compiler type into a different file 2024-02-11 01:55:07 -05:00
Sasha Koshka e8b8139b20 Merge pull request 'fix-chained-methods' (#32) from fix-chained-methods into main
Reviewed-on: sashakoshka/fspl#32
2024-02-11 02:35:37 +00:00
Sasha Koshka fce7595126 New test for generator confirmed to work, filled out 2024-02-10 21:31:10 -05:00
Sasha Koshka 6bb61b294b testcommon last column no longer has trailing spaces 2024-02-10 21:24:09 -05:00
Sasha Koshka 5026576878 Add same test as stub to generator 2024-02-10 21:20:24 -05:00
Sasha Koshka 6aa75b80c5 Fixed crash in analyzer that caused #31 2024-02-10 21:16:22 -05:00
Sasha Koshka 4425823404 Test case code was wrong, methods need to return Number 2024-02-10 21:15:56 -05:00
Sasha Koshka 6e4494e7ba Analyzer test-common now formats errors properly 2024-02-10 21:14:30 -05:00
Sasha Koshka cbbccea64a Add #31 as test case 2024-02-10 21:10:49 -05:00
Sasha Koshka 547b625a3f Add .editorconfig 2024-02-10 19:03:38 -05:00
Sasha Koshka 813a95bc8c OOPS 2024-02-10 18:57:53 -05:00
Sasha Koshka 2c6623e763 Unexported parser 2024-02-10 18:56:25 -05:00
Sasha Koshka 8c6b7a96af Merge pull request 'document-packages' (#30) from document-packages into main
Reviewed-on: sashakoshka/fspl#30
2024-02-10 23:52:17 +00:00
Sasha Koshka 9ca1483f82 Updated spec a little 2024-02-10 18:49:10 -05:00
Sasha Koshka c78deebcc6 Fix small issues with generator readme 2024-02-10 18:32:35 -05:00
Sasha Koshka 9d48bcb868 Add generator README.md 2024-02-10 18:26:51 -05:00
Sasha Koshka 803923b3d2 Bring over better doc on assignment modes to the source file 2024-02-10 14:50:43 -05:00
Sasha Koshka bb77db4c80 Add analyzer README.md 2024-02-10 14:47:47 -05:00
Sasha Koshka 5f804c71da HHHHHHHHH 2024-02-09 22:36:21 -05:00
Sasha Koshka 341bbb9a6e God 2024-02-09 20:23:22 -05:00
Sasha Koshka 877b6d669a Fix mistake in parser readme 2024-02-09 20:22:05 -05:00
Sasha Koshka a230c5d77b Add parser README.md 2024-02-09 20:18:21 -05:00
Sasha Koshka 2dacebefae DESIGN.md -> README.md
Want this to show up in git web interfaces
2024-02-09 18:02:03 -05:00
Sasha Koshka 1804cbbd98 I need to sleep 2024-02-09 04:44:09 -05:00
Sasha Koshka 2e9f5ee11e Enhanced lexer/DESIGN.md 2024-02-09 04:42:03 -05:00
Sasha Koshka 6e85c6ae19 Add DESIGN.md to lexer 2024-02-09 04:37:39 -05:00
Sasha Koshka d10eb4e41e Merge pull request 'dont-create-lexer' (#27) from dont-create-lexer into main
Reviewed-on: sashakoshka/fspl#27
2024-02-09 09:14:29 +00:00
Sasha Koshka 194617dd34 Update fsplc command 2024-02-09 04:05:44 -05:00
Sasha Koshka 5d09926d30 Updated generator's test-common 2024-02-09 04:02:55 -05:00
Sasha Koshka a8a74ec31f Updated analyzer's test-common 2024-02-09 04:01:41 -05:00
Sasha Koshka 7d2831109c Udpated parser's test-common 2024-02-09 03:59:00 -05:00
Sasha Koshka 727ccf3660 Remedy #25 2024-02-09 03:48:44 -05:00
Sasha Koshka ce66fc879a Change parser constructor, tree's parsing methods
Now accept lexers instead of io.Readers
2024-02-09 03:44:29 -05:00
Sasha Koshka 5e3ef3f57c Merge pull request 'document-packages' (#26) from document-packages into main
Reviewed-on: sashakoshka/fspl#26
2024-02-09 08:39:46 +00:00
Sasha Koshka 361c4f7289 Remove llvm/README.md
The information in that file is now a doc comment
2024-02-09 03:37:10 -05:00
Sasha Koshka d59703aa5d Add package level doc comment to llvm 2024-02-09 03:35:42 -05:00
Sasha Koshka 229c88b58a Add module level doc comment to analyzer 2024-02-09 03:30:24 -05:00
Sasha Koshka 503b692104 Add documentation for fsplc command 2024-02-09 03:22:36 -05:00
Sasha Koshka 9bb10ef00d Completed doc comments in entity 2024-02-09 01:11:24 -05:00
Sasha Koshka 097d7dfbe3 Add module level doc comment to testcommon 2024-02-09 01:03:02 -05:00
Sasha Koshka 206a9804dd Add doc comments to integer 2024-02-09 01:00:54 -05:00
Sasha Koshka cd419cb12d Added module-level doc comments to generator and generator/native 2024-02-09 00:57:06 -05:00
Sasha Koshka 3cd512cefb Complete parser doc comments 2024-02-09 00:44:26 -05:00
Sasha Koshka 862e6b1e25 Note for later 2024-02-09 00:44:18 -05:00
Sasha Koshka 5c2df7f837 Added extensive doc comments to errors package 2024-02-08 23:42:04 -05:00
Sasha Koshka b6c7a0482a Complete doc comments in lexer 2024-02-08 23:20:07 -05:00
Sasha Koshka 678f1f15ab Removed lexer.Symbols 2024-02-08 23:06:58 -05:00
Sasha Koshka 2f4522cc6c Added module overview doc comment to lexer 2024-02-08 23:05:36 -05:00
Sasha Koshka 567c59559f Compiler outputs formatted errors 2024-02-08 17:01:38 -05:00
Sasha Koshka 8ff47a3076 Merge pull request 'error-interface' (#23) from error-interface into main
Reviewed-on: sashakoshka/fspl#23
2024-02-08 21:55:47 +00:00
Sasha Koshka fbcc1f14d2 Brought analyzer up to date 2024-02-08 16:53:15 -05:00
Sasha Koshka 93061188e8 Brought parser up to date 2024-02-08 16:51:48 -05:00
Sasha Koshka df9b96eefc Brought lexer up to date 2024-02-08 16:50:42 -05:00
Sasha Koshka 85788fca25 Line and column numbers are entirely handled by errors.Format 2024-02-08 16:48:52 -05:00
Sasha Koshka d2a4d785f6 errors.Error and errors.Position are in two different files 2024-02-08 16:24:46 -05:00
Sasha Koshka 29012110f8 errors.Error is now an interface, implementation is hidden 2024-02-08 16:23:05 -05:00
Sasha Koshka e60a6e246f Unexport generator.resultMode 2024-02-08 16:06:00 -05:00
Sasha Koshka 4089765633 Merge pull request 'remove-participle' (#10) from remove-participle into main
Reviewed-on: sashakoshka/fspl#10
2024-02-08 18:34:18 +00:00
Sasha Koshka 993416d3d8 Merge pull request 'remove-participle-refit-generator' (#9) from remove-participle-refit-generator into remove-participle
Reviewed-on: sashakoshka/fspl#9
2024-02-08 18:33:37 +00:00
Sasha Koshka 24f1044b8f Fixed commaList 2024-02-08 13:32:15 -05:00
Sasha Koshka ee90077f90 Fixed all generator tests 2024-02-08 13:32:04 -05:00
Sasha Koshka 0a87781445 Generator compiles and runs 2024-02-08 13:14:21 -05:00
Sasha Koshka da4d21a87b Go mod tidy. No more participle, officially! 2024-02-08 13:12:42 -05:00
Sasha Koshka 16317a9eb8 Merge pull request 'remove-particple-refit-analyzer' (#8) from remove-particple-refit-analyzer into remove-participle
Reviewed-on: sashakoshka/fspl#8
2024-02-08 18:08:31 +00:00
Sasha Koshka 807732b460 Parser now accepts multiplication operations 2024-02-08 13:05:47 -05:00
Sasha Koshka 5fa787dc64 Fix TestTypeInterfaceBehaviorUniqueErr 2024-02-08 12:57:20 -05:00
Sasha Koshka 775671442b Signature positions are stored properly 2024-02-08 12:54:31 -05:00
Sasha Koshka 498df4fad8 Parser now includes positional information in toplevels 2024-02-08 12:52:47 -05:00
Sasha Koshka 19ba849e4b Bring analyzer tests up to date as much as possible 2024-02-08 12:49:17 -05:00
Sasha Koshka ba52a2df2e Change interface prefix from ? to ~
? will eventually be used in place of * to denote an optional
pointer, and ~ already means "approximation", which works well for
a polymorphic data structure.
2024-02-08 04:10:11 -05:00
Sasha Koshka cb7180fa1c Remove rouge println in parser 2024-02-08 04:00:25 -05:00
Sasha Koshka c9fa465430 Off by one error oh my god 2024-02-08 03:58:29 -05:00
Sasha Koshka 9e815d0bea Updated analyzer's test-common.go 2024-02-08 03:56:09 -05:00
Sasha Koshka 5bb9508a76 Rework analyzer code to fit new parser 2024-02-08 03:51:21 -05:00
Sasha Koshka 090d52b8f8 Merge pull request 'parser-remove-participle' (#7) from parser-remove-participle into remove-participle
Reviewed-on: sashakoshka/fspl#7
2024-02-08 08:29:23 +00:00
Sasha Koshka 95e12d6d19 Parse namespaced function calls 2024-02-08 03:28:09 -05:00
Sasha Koshka 202fd1f0bc Bring most tests up to date 2024-02-08 03:13:40 -05:00
Sasha Koshka ef7be1f7e8 Parse member access, assignment, method calls 2024-02-08 03:10:02 -05:00
Sasha Koshka 68b144a2df Loll oops 2024-02-08 02:22:57 -05:00
Sasha Koshka be314a1b79 Unify entity.Statement and entity.Expression 2024-02-08 02:22:19 -05:00
Sasha Koshka 95118a183b Relaxed unneeded rules on AST 2024-02-08 02:19:00 -05:00
Sasha Koshka 227a329538 Parse bit and value casts 2024-02-08 01:54:29 -05:00
Sasha Koshka e1ae32278f Parse reference operations 2024-02-08 01:37:36 -05:00
Sasha Koshka 335060ce84 Parse length operations 2024-02-08 01:33:55 -05:00
Sasha Koshka 99d4799883 Parse slice operations 2024-02-08 01:29:16 -05:00
Sasha Koshka de1f873929 Struct literals print correctly 2024-02-08 00:56:43 -05:00
Sasha Koshka b715dbcce3 Parse operations 2024-02-08 00:50:54 -05:00
Sasha Koshka e9348ea58f Added string constructor for entity.Operator 2024-02-08 00:29:24 -05:00
Sasha Koshka 5fad8a79dd Re-organize the position of parsing functions in expression.go 2024-02-08 00:16:39 -05:00
Sasha Koshka a0a155c7e6 Parse dereferences and subscripts 2024-02-08 00:14:52 -05:00
Sasha Koshka 1cdc14a313 Lowercased entity descriptions 2024-02-07 22:58:58 -05:00
Sasha Koshka e4c8268e18 Moved parser expression decision tree diagram to expression.go
Because having ASCII flowcharts in comments makes any code base at
least 20% cooler.
2024-02-07 22:53:10 -05:00
Sasha Koshka 1bf47a9ebe Parse function calls 2024-02-07 22:49:11 -05:00
Sasha Koshka 78786f4ea2 Parse break and return expressions 2024-02-07 22:33:33 -05:00
Sasha Koshka 116a687099 Decision tree infrastructure for LBracket branch 2024-02-07 20:03:18 -05:00
Sasha Koshka e0c62e40bf Parse struct literals 2024-02-07 19:48:01 -05:00
Sasha Koshka 1a4bade4dd Parse blocks 2024-02-07 19:24:57 -05:00
Sasha Koshka b17ffdc6d9 Update expression-decision-tree 2024-02-07 19:17:25 -05:00
Sasha Koshka 30d479565b Fix parsing integer 0 2024-02-07 19:16:05 -05:00
Sasha Koshka f997254270 Parse array literals 2024-02-07 19:14:39 -05:00
Sasha Koshka 2882a28621 Update expression-decision-tree 2024-02-07 18:27:10 -05:00
Sasha Koshka ac83753c56 Parse if/else expressions 2024-02-07 18:25:42 -05:00
Sasha Koshka be35cda999 Parse loops 2024-02-07 18:11:12 -05:00
Sasha Koshka 161ee28e7b Move literal parsing routines to a separate file 2024-02-07 18:01:14 -05:00
Sasha Koshka c1df3f5ae5 Add stars in expression-decision-tree to mark what isn't done 2024-02-07 17:57:39 -05:00
Sasha Koshka 8cd0871939 Fix expression parsing control flow 2024-02-07 17:51:44 -05:00
Sasha Koshka ea8d5e6b3a Add parsing boolean and nil literals 2024-02-07 17:25:00 -05:00
Sasha Koshka 87c6490183 Fix commaList 2024-02-07 17:09:35 -05:00
Sasha Koshka 4a3e06362d What crack was I smoking when I made LiteralNil.Value 2024-02-07 15:46:42 -05:00
Sasha Koshka ed239aabb9 Remove entity.Boolean because I have overcome my haters 2024-02-07 15:41:48 -05:00
Sasha Koshka 854abfed1c Broke out Parser.parseVariable() for consistency 2024-02-07 15:36:33 -05:00
Sasha Koshka d445743200 Add parsing int, float, and string literals 2024-02-07 14:15:06 -05:00
Sasha Koshka a47023fc95 Declaration and variable expression parsing 2024-02-07 13:26:25 -05:00
Sasha Koshka 16f27cd2ee Parser panic information is now more detailed 2024-02-07 13:01:25 -05:00
Sasha Koshka cc8426bf85 Add tokens to startTokensExpression 2024-02-07 12:50:19 -05:00
Sasha Koshka 531d3ca22b Created an expression decision tree plan 2024-02-07 12:44:54 -05:00
Sasha Koshka 51ef3682f1 Function headings are now parsed correctly 2024-02-07 11:38:15 -05:00
Sasha Koshka 0762d72486 Allow arbitrary integer width 2024-02-07 11:22:37 -05:00
Sasha Koshka 02ec77b855 Updated TestType test case for parser 2024-02-07 03:39:37 -05:00
Sasha Koshka 97605d8c2a Parse integer, word, and float types 2024-02-07 03:37:02 -05:00
Sasha Koshka 424b621bae Fix function parsing 2024-02-07 03:22:00 -05:00
Sasha Koshka 48fe20b472 Parse interface types 2024-02-07 03:18:02 -05:00
Sasha Koshka 1529e591e8 Parse signatures 2024-02-07 03:17:50 -05:00
Sasha Koshka 19910b77a9 Parse struct types 2024-02-07 03:02:43 -05:00
Sasha Koshka f8399612f6 De-ambiguify interface and struct type syntax 2024-02-07 03:02:16 -05:00
Sasha Koshka cb7734afe6 Array type parsing 2024-02-07 00:32:18 -05:00
Sasha Koshka 23362b058d Normalize parsing of access control symbols 2024-02-07 00:23:10 -05:00
Sasha Koshka 59e3185500 Parse pointers/slices 2024-02-07 00:22:58 -05:00
Sasha Koshka 6ad56ed4ee Add some convenience methods to parser, fix appendCopy 2024-02-07 00:21:44 -05:00
Sasha Koshka 8adaa0e053 Added parsing named types 2024-02-06 23:48:25 -05:00
Sasha Koshka 9dab29f5cb Token.Is and Token.ValueIs work properly 2024-02-06 23:16:48 -05:00
Sasha Koshka 39d8ed088d Lexer now records the width of tokens 2024-02-06 23:08:25 -05:00
Sasha Koshka 84d88a7644 I want to look at my juicy error formatting even when it passes 2024-02-06 22:33:18 -05:00
Sasha Koshka 100986327e FINALLY errors and lexer agree on row/col positions properly 2024-02-06 22:11:46 -05:00
Sasha Koshka 6924d39dcb Fixed error test cases 2024-02-06 21:59:41 -05:00
Sasha Koshka 6fc16c7045 Lexer saves error lines 2024-02-06 21:54:20 -05:00
Sasha Koshka e6043effe0 Lexer tests check for line content, print formatted errors 2024-02-06 19:08:21 -05:00
Sasha Koshka f3f8b324dc Parser errors make more sense 2024-02-06 18:58:09 -05:00
Sasha Koshka c2d9b4d40f Added Token.String() 2024-02-06 18:56:04 -05:00
Sasha Koshka ef14fc2219 Parser is incomplete but compiles 2024-02-06 18:29:37 -05:00
Sasha Koshka 825e71a720 Created a plan for expression parsing 2024-02-06 17:04:59 -05:00
Sasha Koshka 77783c1004 Add a guide to writing parser methods so I don't forget 2024-02-06 16:56:10 -05:00
Sasha Koshka c1c2ab5124 Further normalize expectation responsibilities of parsing methods 2024-02-06 16:39:15 -05:00
Sasha Koshka a09a0ce34c Typedef parsing complete, without detail 2024-02-06 16:32:14 -05:00
Sasha Koshka 76c129f91c Add type parsing stub 2024-02-06 16:31:53 -05:00
Sasha Koshka f4e5e1ea3d Appended -Core to parsing methods that only partially parse things 2024-02-06 16:30:53 -05:00
Sasha Koshka bca9de4106 Parsing functions *always* begin on current token, leave trailing one 2024-02-06 16:22:49 -05:00
Sasha Koshka 1b50c8f879 Method and function parsing complete, without detail 2024-02-06 16:13:05 -05:00
Sasha Koshka 3c67cf576c Expression parsing stub 2024-02-06 16:12:51 -05:00
Sasha Koshka 79f51ce4c6 Parser.expectValue() and Parser.expectValueDesc() take in a kind parameter now 2024-02-06 15:57:27 -05:00
Sasha Koshka 3e89953d07 Add parser.Tree.AddDeclaration() 2024-02-06 15:54:21 -05:00
Sasha Koshka 08f1027834 Added commaList function 2024-02-06 12:20:13 -05:00
Sasha Koshka 118e7672bd Why did I name a prepend function appendr?? 2024-02-06 11:45:08 -05:00
Sasha Koshka 4a2671c601 askl;d;sldkf 2024-02-06 02:55:39 -05:00
Sasha Koshka ac433f07ec Rename typeStartTokens to startTokensType 2024-02-06 02:41:45 -05:00
Sasha Koshka 3d1bef3a32 Move top-level expect from Parser.parseTopLevel into Parser.parse() 2024-02-06 02:40:19 -05:00
Sasha Koshka eafc834ef3 Parser.parseSignature stub 2024-02-06 02:36:51 -05:00
Sasha Koshka 8cfb4f3e6d Parser top-level entity base 2024-02-06 02:32:29 -05:00
Sasha Koshka cce8c756fc Parser now remembers last token 2024-02-06 02:28:23 -05:00
Sasha Koshka 66f0e2d8d9 Un-participle-ify entity.Access 2024-02-06 02:27:09 -05:00
Sasha Koshka 5fe6e32f92 Add Dot and DoubleDot as separate tokens 2024-02-06 02:23:15 -05:00
Sasha Koshka 7db231a945 Add ValueIs method to lexer.Token 2024-02-06 02:19:09 -05:00
Sasha Koshka ec3907d841 Add TokenKind.String 2024-02-06 01:52:29 -05:00
Sasha Koshka b6a920af58 Added expect functions to parser 2024-02-06 01:45:30 -05:00
Sasha Koshka ff9c275980 Add Token.Is() to check if token is any of a set of kinds 2024-02-06 01:31:52 -05:00
Sasha Koshka 04e5ead3db Go mod tidy 2024-02-06 01:14:38 -05:00
Sasha Koshka 0d79482568 Define basic parser structure 2024-02-06 01:12:07 -05:00
Sasha Koshka dc6a36b548 Cleared particple code from parser 2024-02-06 00:52:31 -05:00
Sasha Koshka 57d1ab9b2a Parser test-common uses testcommon package 2024-02-06 00:49:28 -05:00
Sasha Koshka 50ec220d08 Remove entity participle struct tags 2024-02-05 15:17:59 -05:00
Sasha Koshka 187ed48c4f Remove entity dependency on participle 2024-02-05 15:16:11 -05:00
Sasha Koshka 031f7008fa Merge pull request 'lexer-remove-participle' (#3) from lexer-remove-participle into remove-participle
Reviewed-on: sashakoshka/fspl#3
2024-02-05 20:06:38 +00:00
Sasha Koshka d2ecd7a266 Lexer has no dependency on participle now 2024-02-05 15:04:54 -05:00
Sasha Koshka 6bfc073608 Added new Errorf function, added row/column offset 2024-02-05 15:03:13 -05:00
Sasha Koshka db2b7e5f90 Error package now formats tabs correctly 2024-02-05 11:44:34 -05:00
Sasha Koshka 3e9f3553fe TODO message in errors package 2024-02-01 03:21:23 -05:00
Sasha Koshka 7c29d552f2 Wrote tests for and fixed errors package 2024-02-01 03:20:17 -05:00
Sasha Koshka e3f5ffa5c5 Added erros package 2024-02-01 02:51:04 -05:00
Sasha Koshka 554fc1f7a7 Put functionality from generator/test-common.go in its own package 2024-02-01 01:59:17 -05:00
Sasha Koshka b95df4c23c Fix crash when using int literals as floats 2024-01-29 13:29:53 -05:00
Sasha Koshka 8340dbe7c2 Update tests 2024-01-29 03:30:54 -05:00
Sasha Koshka 0d20f1cbc0 Slice length fields are properly stored as Index values 2024-01-29 03:11:43 -05:00
Sasha Koshka af79419809 Update tests 2024-01-29 02:20:16 -05:00
Sasha Koshka 0c1382896e Updated test cases for literals 2024-01-29 00:30:48 -05:00
Sasha Koshka 1e827a9ea1 Array literals and string literals now exhibit identical behavior
They both fill unspecified data with zeros, and add a null
terminator when directly assigned to a pointer.
2024-01-29 00:25:34 -05:00
Sasha Koshka 06a183c7af String generation now uses correct type names, data length, and nulls 2024-01-28 17:10:03 -05:00
Sasha Koshka 9b80c29a15 Fix tests 2024-01-28 16:44:34 -05:00
Sasha Koshka 044e73efda Add source code printout for tests 2024-01-28 14:46:47 -05:00
Sasha Koshka 8107f82531 Broke up the monolithic TestLiteral 2024-01-28 14:36:04 -05:00
Sasha Koshka e42c292433 Increased the coverage of TestLiteralArray 2024-01-28 14:15:54 -05:00
Sasha Koshka c71cc43a47 *Greatly* reduced the amount of excess IR related to string literals 2024-01-28 14:11:41 -05:00
Sasha Koshka 0d4a86ef02 Struct literals now support irDestLoc 2024-01-28 04:50:18 -05:00
Sasha Koshka 82c05039b7 Added the ability for array literals to be undersized 2024-01-28 04:43:01 -05:00
Sasha Koshka 86df537ece Added test for nested array literals
Who knew it would work first try?
2024-01-28 04:24:15 -05:00
Sasha Koshka f057f3b4a4 Array literals now support irDestLoc 2024-01-28 04:18:33 -05:00
Sasha Koshka 85703f6059 Add more tests for loops 2024-01-28 03:29:49 -05:00
Sasha Koshka fe85582088 Fixed extra lines after IR blocks 2024-01-28 03:18:39 -05:00
Sasha Koshka 5940e540b3 Fix some issues with control flow, add tests 2024-01-28 03:16:05 -05:00
Sasha Koshka 3c7565861d Add test case for comparisons on defined types 2024-01-28 02:18:18 -05:00
Sasha Koshka cd6ed93b98 FCmp and ICmp now accept defined types 2024-01-28 00:18:37 -05:00
Sasha Koshka 56ae34c297 Updated all tests except for literal_test.go
You Will See Why
2024-01-27 23:24:19 -05:00
Sasha Koshka 57ef5933ee Well it would help if it generated well formed IR 2024-01-27 23:00:03 -05:00
Sasha Koshka f35f3e0df0 Assignment coercions are much more IR efficient 2024-01-27 22:45:28 -05:00
Sasha Koshka 773e718592 Complete TestInterfaceInStruct (no longer segfaults somehow) 2024-01-27 19:49:22 -05:00
Sasha Koshka 41c9f57063 More using qualified names in IR 2024-01-27 18:16:30 -05:00
Sasha Koshka d9c6bd644b Oh my god I swapped the columns 2024-01-27 18:09:51 -05:00
Sasha Koshka 954f6c877e Added cool output to failed tests 2024-01-27 18:07:49 -05:00
Sasha Koshka 05e75cb746 Literal generation now uses qualified type names when possible 2024-01-27 17:16:23 -05:00
Sasha Koshka 3a4a5683b5 Remove redundant val variable/declaration generation 2024-01-27 14:17:52 -05:00
Sasha Koshka f994c4dc08 Rename some routines in data 2024-01-27 14:03:38 -05:00
Sasha Koshka 15541b8675 Merge pull request 'generator-reduce-redundant-ir' (#2) from generator-reduce-redundant-ir into main
Reviewed-on: sashakoshka/fspl#2
2024-01-27 17:40:37 +00:00
Sasha Koshka 805a9a110f Updated more tests (cutting out extra load insts) 2024-01-27 17:35:33 +00:00
Sasha Koshka 91d4b92e6b Fix more names and orgaization of statement generation 2024-01-27 17:33:15 +00:00
Sasha Koshka 021a289623 Added -Any suffix to appropriate generation routines 2024-01-27 17:24:47 +00:00
Sasha Koshka 5085ea8502 Fixed segfault relating to break and return statements 2024-01-27 17:11:44 +00:00
Sasha Koshka b13130db8e Introducing ResultMode! time to got to bed. 2024-01-27 10:43:27 +00:00
Sasha Koshka 21fd6a4130 Update some tests 2024-01-27 10:06:17 +00:00
Sasha Koshka 706e2e0286 Updated control flow generation 2024-01-27 10:03:15 +00:00
Sasha Koshka 6d28d52cdf Void functions/methods no longer request a value 2024-01-27 09:22:19 +00:00
Sasha Koshka fc5dda739e Made generic block generation routine 2024-01-27 09:18:41 +00:00
Sasha Koshka 8bf3938842 Made panic messages in expression-multiplex.go more informative 2024-01-27 09:15:39 +00:00
Sasha Koshka 43130fd0da Add Val onto value expressions 2024-01-27 09:09:27 +00:00
Sasha Koshka cc5ed3c2ee Update block generation routines 2024-01-27 09:06:17 +00:00
Sasha Koshka b1799c25e1 Add generateStatement routine 2024-01-27 08:53:29 +00:00
Sasha Koshka 1f653be8cc Renamed generateStatement to generateStatementVal 2024-01-27 08:49:25 +00:00
Sasha Koshka 43aae56b37 Add generateExpression function 2024-01-27 08:48:00 +00:00
Sasha Koshka f9d53d7ab7 Rename expression source files 2024-01-27 08:40:02 +00:00
Sasha Koshka fbaec1efdb Rename generateExpression to generateExpressionVal 2024-01-27 08:39:05 +00:00
Sasha Koshka 10fb780e50 Nitpick 2024-01-27 08:26:46 +00:00
Sasha Koshka 6de2a9d37d Change restricted access modifier to tilde (~) 2024-01-27 07:42:13 +00:00
Sasha Koshka 8606c761e8 Same thing for bit casts 2024-01-27 07:05:34 +00:00
Sasha Koshka b56dceb3e1 Generator now no longer generates ineffectual value casts 2024-01-27 07:03:13 +00:00
Sasha Koshka 7318bfbc88 Added a way to test IR type equality 2024-01-27 06:49:30 +00:00
Sasha Koshka ca6be75cf0 Merge pull request 'llvm-implement-globals' (#1) from llvm-implement-globals into main
Reviewed-on: sashakoshka/fspl#1
2024-01-27 04:33:39 +00:00
Sasha Koshka 9ac9af8bcb Add IsConstant to Global 2024-01-27 04:16:41 +00:00
Sasha Koshka ed350f94b8 Add basic globals implementation 2024-01-27 01:49:18 +00:00
Sasha Koshka 709ec56873 Move addrspace to attribute file, will put more things in there 2024-01-27 01:48:29 +00:00
Sasha Koshka 6b9c165d14 Add function to encode global identifier 2024-01-27 01:47:52 +00:00
Sasha Koshka b0c62642f5 Add more tests to generator 2024-01-27 01:00:07 +00:00
Sasha Koshka 4c66ad4b44 Replace bugs section with caveats in README 2024-01-26 22:11:35 +00:00
Sasha Koshka 35dbbebb8b Updated stuff to use the new routines in data.go 2024-01-26 05:35:47 +00:00
Sasha Koshka 098248da19 More routines in data.go 2024-01-26 05:35:22 +00:00
Sasha Koshka a80cd600b5 Added a test case for a Writer interface
I really need to cut out a lot of the unnecessary IR that the
generator emits
2024-01-26 04:32:55 +00:00
Sasha Koshka ea32fcc7d5 Added a test case for casting slices to pointers 2024-01-26 04:21:30 +00:00
Sasha Koshka d28bfbac16 Fix conversion of slices to pointers 2024-01-26 04:07:58 +00:00
Sasha Koshka 34b9812db4 Same thing as last commit 2024-01-26 04:05:58 +00:00
Sasha Koshka 85a582e349 Add more comments to explain casting rules in analyzer 2024-01-26 00:40:22 +00:00
Sasha Koshka 8e458111b5 Change the location at which defined IR types are wrapped
Defined IR types are wrapped in a TypeDefined by generateTypeNamed.
This causes them to be sent through GEP, which is necessary for
testing.
2024-01-25 22:57:52 +00:00
Sasha Koshka f6604dcecd Wrote test for getelementptr'ing into chained typedefs 2024-01-25 18:19:34 +00:00
Sasha Koshka 67a0157cbd Use TypeDefined in generator 2024-01-25 18:19:22 +00:00
Sasha Koshka 2a927b209f Added a way to wrap a defined type via TypeDefined
This allows "copies" of a type that refer to a source to be created
and have their names changed/redefined without modifying the source
type.
2024-01-25 18:08:26 +00:00
Sasha Koshka f1d48d74a2 Add test for chained typedefs with different names 2024-01-25 17:56:20 +00:00
Sasha Koshka 5b5c66d409 I forgot comments existed haha 2024-01-25 08:27:18 +00:00
Sasha Koshka 699cd88000 Nicer testing framework for lexer 2024-01-25 08:27:17 +00:00
Sasha Koshka 54ac39ee01 Lexer now accepts files with no blank line at the end 2024-01-25 08:27:17 +00:00
Sasha Koshka a0d31fad38 Update README.md 2024-01-25 07:43:21 +00:00
Sasha Koshka b20dbcebbb Add logo to readme 2024-01-25 07:42:32 +00:00
Sasha Koshka bb59f8de4a Fix duplicated pointer types 2024-01-25 07:36:51 +00:00
Sasha Koshka 4b28d40b89 AbstractType's TypeName member is now private 2024-01-25 07:05:07 +00:00
Sasha Koshka 9390eb6b2e Added test to debug absurd behavior relating to type defs 2023-12-24 22:49:10 -05:00
Sasha Koshka 70c45f6fe1 Interface method calls no longer cause a segfault 2023-12-24 22:18:47 -05:00
Sasha Koshka 7757133d43 Something was off with TestCompare 2023-12-24 21:45:47 -05:00
Sasha Koshka 7d926be83a Fix code I probably wrote while high off my ass 2023-12-24 21:39:56 -05:00
Sasha Koshka d4f81b0667 Empty struct literal values are initialized to zero 2023-12-23 17:25:27 -05:00
Sasha Koshka 3cca2dd130 Added nil (zero initializer) literals 2023-12-21 23:33:38 -05:00
Sasha Koshka eeae7ac1ed Generator can produce link names 2023-12-20 03:05:27 -05:00
Sasha Koshka af73d8e251 Added module referencing to parser 2023-12-19 15:21:10 -05:00
Sasha Koshka 308efbbd5e Add access specifiers and link names to parser 2023-12-17 11:23:42 -05:00
Sasha Koshka ae60aa173e Made the type checker a tad more robust 2023-12-15 13:23:37 -05:00
Sasha Koshka 6bc59ca0d1 I forgor 2023-12-15 12:46:33 -05:00
Sasha Koshka 7572e8bf08 Fix comparison predicates 2023-12-15 12:40:43 -05:00
Sasha Koshka da7807d653 Attempt to further fix interface assignment 2023-12-14 21:21:07 -05:00
Sasha Koshka b7faa727af Method calling now properly passes pointer instead of value 2023-12-14 01:23:34 -05:00
Sasha Koshka 4a28ee61c0 generateAssignmentSource assigns interface methods to iface dest 2023-12-14 00:47:14 -05:00
Sasha Koshka 62a83f5544 Add some extra notes to readme 2023-12-13 03:44:57 -05:00
Sasha Koshka c9eccdd86f Oops pt 2 2023-12-13 03:40:40 -05:00
Sasha Koshka e796f055ad Oops 2023-12-13 03:38:44 -05:00
Sasha Koshka 895e874e2f Compiler can fall back to clang when llc is not present 2023-12-13 03:28:24 -05:00
Sasha Koshka 5cd1be88ce Add logo 2023-12-13 03:20:15 -05:00
Sasha Koshka 20db37b704 Changed my mind about brackets on break and return statements 2023-12-12 19:51:17 -05:00
Sasha Koshka 5dc9331668 Updated language spec 2023-12-08 17:31:16 -05:00
Sasha Koshka 35ecdd7a8b Add README.md 2023-12-08 00:28:58 -05:00
Sasha Koshka ea934445c6 Add test for aforementioned issue 2023-12-07 17:14:37 -05:00
Sasha Koshka 139a66f239 Fix IsUnsigned 2023-12-07 17:05:26 -05:00
Sasha Koshka 8c64c07fff If statements get a type 2023-12-07 04:16:58 -05:00
Sasha Koshka 019a8732cb Augh 2023-12-07 04:03:28 -05:00
Sasha Koshka 8d1d87dbb8 If/else branching is no longer... like how it was 2023-12-07 03:58:57 -05:00
Sasha Koshka 1176da7cb9 If/else generation will not attempt to overwrite branches 2023-12-07 03:54:01 -05:00
Sasha Koshka 988649098c Operands to IR binary instructions now have identifiers instead of names 2023-12-07 03:04:29 -05:00
Sasha Koshka e470fb1f5c Generator alloca's variables when they are defined 2023-12-07 02:48:27 -05:00
Sasha Koshka 2353ecd0e1 Fix case where generator would assign ID to void function calls 2023-12-05 22:36:41 -05:00
Sasha Koshka e32d349e2e Add some more test cases to the generator 2023-12-05 22:29:29 -05:00
Sasha Koshka 8bc00720ac Fix bug with popping block managers off of the stack 2023-12-05 22:27:55 -05:00
Sasha Koshka 058a985d92 Add "internal error" prefix for generator errors 2023-12-05 22:08:06 -05:00
Sasha Koshka f7a6e905f6 Fix comments 2023-12-05 22:04:12 -05:00
Sasha Koshka 6074b414b5 Add comments 2023-12-05 16:52:33 -05:00
Sasha Koshka 588800e02c Slice operations now load the data pointer 2023-12-05 15:35:17 -05:00
Sasha Koshka 5205f01877 Revert slices back to having just two fields 2023-12-05 03:07:54 -05:00
Sasha Koshka b0c3839ed9 Fixed several cases where the generator would output invalid IR 2023-12-03 22:23:04 -05:00
Sasha Koshka a7fe9a592b Generator passes method owner as pointer 2023-12-03 15:38:07 -05:00
Sasha Koshka ab8d616e2b Fix stale code 2023-12-02 23:42:54 -05:00
Sasha Koshka b1cd82d4ed Generator generates methods properly 2023-12-02 23:41:35 -05:00
Sasha Koshka c0997b3e47 Oops 2023-12-02 23:36:12 -05:00
Sasha Koshka fa69bd2294 Generator now pierces pointers when accessing members as well 2023-12-02 23:35:22 -05:00
Sasha Koshka 5f30623e8f Member access can now pierce through pointers in the analyzer 2023-12-02 23:23:44 -05:00
Sasha Koshka 65ec3c4ad0 Analyzer adds a "this" pointer to methods 2023-12-02 23:06:58 -05:00
Sasha Koshka 3b6aa7fffb String literals can be assigned to strings now 2023-12-02 22:45:22 -05:00
Sasha Koshka 55a86ad64c That should be literally every feature 2023-12-02 21:45:15 -05:00
Sasha Koshka 5539f8722f Created function for making slices 2023-12-02 04:21:10 -05:00
Sasha Koshka 67eb74c66e Ditch brackets around break and return statements 2023-12-01 01:53:33 -05:00
Sasha Koshka 9ccb1490bf Completed operations 2023-12-01 01:18:10 -05:00
Sasha Koshka ccd3ddd82a Made native target actually work 2023-11-30 17:16:55 -05:00
Sasha Koshka af4827590e Operation stub part II 2023-11-30 15:44:30 -05:00
Sasha Koshka 1c5a58aa23 Add operation stub 2023-11-30 02:13:47 -05:00
Sasha Koshka f87a4a4b78 Added casting 2023-11-30 02:06:12 -05:00
Sasha Koshka c24113ba51 Add length to analyzer and parser 2023-11-30 02:05:32 -05:00
Sasha Koshka 78d4276a57 Added bit casts 2023-11-29 20:37:56 -05:00
Sasha Koshka a58b058025 Generator properly uses subscript offset 2023-11-29 20:31:40 -05:00
Sasha Koshka 593683156a Generator grabs slice type from proper place 2023-11-29 20:31:07 -05:00
Sasha Koshka b96d9c6c4c Multi-layer location expression type inference 2023-11-29 20:07:16 -05:00
Sasha Koshka 0b4aef1530 Analyzer does light upward type inference for location expressions 2023-11-29 18:52:27 -05:00
Sasha Koshka c5a61e15f6 Generator now adds null terminator to c-strings 2023-11-29 00:45:38 -05:00
Sasha Koshka 37819645cd Reduce element type when generating string literal data 2023-11-29 00:21:58 -05:00
Sasha Koshka 0afccbe6b1 Facehoof 2023-11-29 00:13:54 -05:00
Sasha Koshka ced43a161d Added string literals to generator 2023-11-29 00:01:35 -05:00
Sasha Koshka c08f5d7706 Add string literals to generator WIP 2023-11-28 01:15:12 -05:00
Sasha Koshka 9808fc0e7c Added string literal to analyzer 2023-11-26 17:27:31 -05:00
Sasha Koshka 905522af56 Added string literal to parser 2023-11-26 05:16:40 -05:00
Sasha Koshka fddccf2967 Add escape sequences to strings in lexer 2023-11-26 04:56:14 -05:00
Sasha Koshka aa107072d8 This should have been way more commits 2023-11-26 04:02:22 -05:00
Sasha Koshka 3a751666ba Fixed scope OverVariables stopping immediately 2023-11-26 04:01:55 -05:00
Sasha Koshka f08a09cee2 fsplc command now infers output type from output file name ext 2023-11-26 04:01:03 -05:00
Sasha Koshka ae9bace68a Analyzer no longer deletes dereferences 2023-11-26 04:00:44 -05:00
Sasha Koshka 36e70c0c97 LLVM improvements 2023-11-26 04:00:20 -05:00
Sasha Koshka 0a54cbdd63 Typedefs that are never referenced are never generated 2023-11-23 01:44:48 -05:00
Sasha Koshka 8258c4f400 Basic compiler command 2023-11-23 01:26:28 -05:00
Sasha Koshka 94bf21536d Generator returns an LLVM module 2023-11-23 01:26:14 -05:00
Sasha Koshka 15fece8154 Various fixes 2023-11-23 00:02:00 -05:00
Sasha Koshka a2d4252428 Migrated generator to use new llvm code generation package 2023-11-22 20:37:37 -05:00
Sasha Koshka 0efc4d209e Add custom LLVM code generation package 2023-11-22 20:37:16 -05:00
Sasha Koshka 0331b486bf Generator WIP 2023-11-21 15:04:22 -05:00
Sasha Koshka ff9fdfeab0 Slight modifications to analyzer 2023-11-21 15:04:01 -05:00
Sasha Koshka 05e298583e Functions/behaviors stub 2023-11-16 22:41:20 -05:00
Sasha Koshka 652a6c313b Add CLI stub 2023-11-16 22:40:57 -05:00
Sasha Koshka 729af04963 Add type generator 2023-11-16 22:04:56 -05:00
Sasha Koshka 13b18c0ce0 Generator stub II 2023-11-16 15:52:28 -05:00
Sasha Koshka 3f96704723 Generator stub 2023-11-14 02:49:55 -05:00
Sasha Koshka 5e4d2a32e4 Pass all analyzer tests 2023-11-04 17:59:04 -04:00
Sasha Koshka 9706e844d6 Pass all but one test 2023-11-04 16:34:40 -04:00
Sasha Koshka 162487490e Fix goofy bugs 2023-10-31 21:52:47 -04:00
Sasha Koshka b6a7b237ba uhhhh 2023-10-31 15:43:19 -04:00
Sasha Koshka 58513e7d15 Can now assign array directly to slice 2023-10-31 01:46:18 -04:00
Sasha Koshka 1a09e9ba2a Pass all literal assignment tests 2023-10-29 15:18:44 -04:00
Sasha Koshka b839517e9a Array literal can be used in array and slice 2023-10-29 15:13:25 -04:00
Sasha Koshka 45574d3cd5 Pass a bunch of tests 2023-10-29 15:09:20 -04:00
Sasha Koshka 8ce12b8a71 Removed Void as a concept 2023-10-29 14:47:28 -04:00
Sasha Koshka 7b0d2d50bd Expression analysis actually runs now 2023-10-29 14:40:29 -04:00
Sasha Koshka 65a32c3cdb Fix entity string conversion 2023-10-29 14:34:21 -04:00
Sasha Koshka e47096c219 Analyze return statement 2023-10-29 14:28:51 -04:00
Sasha Koshka b884978b87 Analyze break statements 2023-10-29 02:59:45 -04:00
Sasha Koshka 9e85f869d5 Analyze more expressions 2023-10-29 02:18:41 -04:00
Sasha Koshka 7df9517788 Add loop stack management to scopeManager and scopeContextManager 2023-10-29 02:18:03 -04:00
Sasha Koshka 8cb7e2d8b0 Analyze all the literals 2023-10-28 03:24:17 -04:00
Sasha Koshka 8a5d08d7fc Revise spec a bit 2023-10-28 03:24:08 -04:00
Sasha Koshka 24ae32c95a Start analyzing literals 2023-10-27 16:10:31 -04:00
Sasha Koshka 7d8a1cd714 Add package for determining integer sizes 2023-10-27 16:10:05 -04:00
Sasha Koshka 61a04d51a4 More expressions (wip) 2023-10-26 10:43:50 -04:00
Sasha Koshka fb5d17742e Analysis of value/bit casts 2023-10-25 01:57:02 -04:00
Sasha Koshka 2a5ffb12a1 Location expressions 2023-10-24 21:54:59 -04:00
Sasha Koshka bec91323b9 Added analysis for numerous types of expressions 2023-10-24 18:29:33 -04:00
Sasha Koshka 5a48415bd5 I forgor 2023-10-24 01:34:58 -04:00
Sasha Koshka 4448d3ccb4 Function call analysis 2023-10-20 13:48:05 -04:00
Sasha Koshka 5d4977828e Added basic equality checks to types 2023-10-19 21:53:19 -04:00
Sasha Koshka 84f76f8ba4 WIP expression analysis type checking 2023-10-19 19:13:55 -04:00
Sasha Koshka 13d281388a New syntax for method calls
This will free up the '::' token for namespace/module access. Exciting!
2023-10-19 17:19:23 -04:00
Sasha Koshka bed87812d5 Expression analysis stub 2023-10-18 02:29:24 -04:00
Sasha Koshka 7c0e96777b That, but for operation arg count testing 2023-10-18 01:39:43 -04:00
Sasha Koshka 87dcc18a8d Updated analyzer tests to account for lexer changes 2023-10-18 01:33:22 -04:00
Sasha Koshka c375c18922 Star is now a separate token 2023-10-18 01:14:17 -04:00
Sasha Koshka f65f9e042c Implement custom lexer FINALLY 2023-10-18 00:48:35 -04:00
Sasha Koshka 6ee91bee08 Finished analyzing method signatures 2023-10-17 03:21:25 -04:00
Sasha Koshka 416c6b1d59 hngnggg i forgor to commit
- implemented scope management
- finished function signature analyzing
- added method analyzing
  - had to restructure type analysis slightly to do this
2023-10-17 03:11:11 -04:00
Sasha Koshka 90796775d3 Booleans are now literals 2023-10-09 00:24:48 -04:00
Sasha Koshka 8cc9b88126 Function analysis stub 2023-10-09 00:13:13 -04:00
Sasha Koshka 4b2158b1f0 Added recursive type definitions 2023-10-08 23:54:45 -04:00
Sasha Koshka fcf4eba79e All the builtin/primitive types are in their own place 2023-10-07 17:32:47 -04:00
Sasha Koshka feb0da1172 Testing ALL the types 2023-10-07 02:27:49 -04:00
Sasha Koshka 47fb051caa Add readme 2023-10-07 01:55:33 -04:00
Sasha Koshka 0113536b0d the ansalyzer 2023-10-07 01:18:05 -04:00
Sasha Koshka 160cbd16ad Reorganized tests 2023-10-06 18:58:17 -04:00
160 changed files with 18421 additions and 1338 deletions

12
.editorconfig Normal file
View File

@ -0,0 +1,12 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
indent_style = tab
indent_size = 8
charset = utf-8
[*.md]
indent_style = space
indent_size = 2

674
LICENSE Normal file
View File

@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

119
README.md Normal file
View File

@ -0,0 +1,119 @@
# FSPL
<img src="assets/fspl.svg" width="128" alt="FSPL logo.">
Freestanding programming language: a high-ish-level language that has absolutely
no need for any runtime support, designed to work well in scenarios where
dragging along a language runtime is either not possible or simply unwanted.
This language is designed for:
- Operating system development
- Embedded software
- Integrating cleanly into existing software
## Design Principles
- Abstractions must happen at compile time unless absolutely necessary
- Compiler must not generate any functions that the user does not write
- Compiler must avoid generating logic that the user does not write
## Installation
You can install the compiler by running:
`go install ./cmd/fsplc`
The `fsplc` program depends on the LLVM IR compiler (`llc`). If it is not found,
it will attempt to use `clang` instead but with some features disabled. Please
ensure either `llc` or `clang` are installed and accessible from your PATH
before using this software.
## Usage
The `fsplc` program may be used as follows:
`fsplc [ARGUMENT(S)...] FILE(S)...`
The program compiles all input files into one output. The output file type is
determined by the filename extension of the output file:
| Extension | Type |
| --------- | --------------- |
| .s | Native assembly |
| .o | Object file |
| .ll | LLVM IR |
If no output file is specified, it will default to an object file with the name
of the first input file.
Object files can be linked into an executable binary using the linker of your
choice, or by using a C compiler such as `clang`:
`clang -o OUTPUT INPUT.o`
Using a C compiler will link the C standard library to your program, which may
be useful for building normal user applications.
## Learning the language
At this time, there is no guided method of learning how to write FSPL code.
However, a good place to start is the `design` directory, which contains a
language specification among other things. The language specification goes into
detail about the syntax and semantics of the language, and assuming some
background in C programming, it should be enough to attain a reasonable grasp
of the language.
## Caveats, Bugs
Note that the compiler is still relatively early in development, and has
numerous bugs. In addition, language features and syntax are not yet set in
stone and may change in the future. Please report any bugs you find to the
[issue tracker](https://git.tebibyte.media/sashakoshka/fspl/issues).
## Roadmap
Late 2023 (These have been implemented):
- Top-level entities
- Type definitions
- Methods
- Defined and external functions
- Type system
- Strict, static, bottom-up type inference
- Pointers
- Arrays
- Slices
- Structs
- Interfaces
- Expressions and control structures
- Literals adapt to types via bottom-up type inference
- Assignment
- Variable declaration
- Variable access
- Function calls
- Method calls
- Interface behavior calls
- Operations
- Casting
- Blocks
- If/else
- Loops
Q1 2024:
- Union types (carry type information)
- Match statements
- Modules
- Mutable/immutable variables
- For/range loops
Q2 2024:
- Basic, non-final standard library routines
- Conditional compilation
- Shared library compilation
- Constants
- Vararg
- FSPL vararg using Slices
- Optional per-function C-style vararg for compatibility
Q3 2024:
- Generics
- Ownership system
- Lightweight, modularized (and of course, totally optional) standard library to replace those written in Q2
At the beginning of Q4 2024, a 1.0 version of the language will be released.

89
analyzer/README.md Normal file
View File

@ -0,0 +1,89 @@
# analyzer
## Responsibilities
- Define syntax tree type that contains entities
- Turn streams of tokens into abstract syntax tree entities
## Organization
The entry point for all logic defined in this package is the Tree type. On this
type, the Analyze() method is defined. This method checks the semantic
correctness of an AST, fills in semantic fields within its data structures, and
arranges them into the Tree.
Tree contains a scopeContextManager. The job of scopeContextManager is to manage
a stack of scopeContexts, which are each tied to a function or method that is
currently being analyzed. In turn, each scopeContext manages stacks of
entity.Scopes and entity.Loops. This allows for greedy/recursive analysis of
functions and methods.
## Operation
When the analyze method is called, several hidden fields in the Tree are filled
out. Tree.ensure() instantiates data that can persist between analyses, which
consists of map initialization and merging the data in the builtinTypes map into
Tree.Types.
After Tree.ensure completes, Tree.assembleRawMaps() takes top-level entities
from the AST and organizes them into rawTypes, rawFunctions, and rawMethods. It
does this so that top-level entites can be indexed by name. While doing this, it
ensures that function and type names are unique, and method names are unique
within the type they are defined on.
Next, Tree.analyzeDeclarations() is called. This is the entry point for the
actual analysis logic. For each item in the raw top-level entity maps, it calls
a specific analysis routine, which is one of:
- Tree.analyzeTypedef()
- Tree.analyzeFunction()
- Tree.analyzeMethod()
These routines all have two crucial properties that make them very useful:
- They refer to top-level entities by name instead of by memory location
- If the entity has already been analyzed, they return that entity instead of
analyzing it again
Because of this, they are also used as accessors for top level entities within
more specific analysis routines. For example, the routine Tree.analyzeCall()
will call Tree.analyzeFunction() in order to get information about the function
that is being called. If the function has not yet been analyzed, it is analyzed
(making use of scopeContextManager to push a new scopeContext), and other
routines (including Tree.analyzeDeclarations()) will not have to analyze it all
over agian. After a top-level entity has been analyzed, these routines will
always return the same pointer to the one instance of the analyzed entity.
## Expression Analysis and Assignment
Since expressions make up the bulk of FSPL, expression analysis makes up the
bulk of the semantic analyzer. Whenever an expression needs to be analyzed,
Tree.analyzeExpression() is called. This activates a switch to call one of many
specialized analysis routines based on the expression entity's concrete type.
Much of expression analysis consists of the analyze checking to see if the
result of one expression can be assigned to the input of another. To this end,
assignment rules are used. There are five different assignment modes:
- Strict: Structural equivalence, but named types are treated as opaque and are
not tested. This applies to the root of the type, and to types enclosed as
members, elements, etc. This is the assignment mode most often used.
- Weak: Like strict, but the root types specifically are compared as if they
were not named. analyzer.ReduceToBase() is used to accomplish this.
- Structural: Full structural equivalence, and named types are always reduced.
- Coerce: Data of the source type must be convert-able to the destination type.
This is used in value casts.
- Force: All assignment rules are ignored. This is only used in bit casts.
All expression analysis routines take in as a parameter the type that the result
expression is being assigned to, and the assignment mode. To figure out whether
or not they can be assigned, they in turn (usually) call Tree.canAssign().
Tree.canAssign() is used to determine whether data of a source type can be
assigned to a destination type, given an assignment mode. However, it is not
called automatically by Tree.analyzeExpression() because:
- Determining the source type is sometimes non-trivial (see
Tree.analyzeOperation())
- Literals have their own very weak assignment rules, and are designed to be
assignable to a wide range of data types

View File

@ -1,320 +1,500 @@
package analyzer
import "testing"
import "fmt"
import "math"
import "git.tebibyte.media/fspl/fspl/errors"
import "git.tebibyte.media/fspl/fspl/entity"
import "git.tebibyte.media/fspl/fspl/integer"
func TestAssignLiteralErrUnsignedNegative (test *testing.T) {
testStringErr (test,
"expected unsigned integer", 3, 11,
`
[main] = {
x:UInt = -5
}
`)
type strictness int; const (
// Structural equivalence, but named types are treated as opaque and are
// not tested. This applies to the root of the type, and to types
// enclosed as members, elements, etc. This is the assignment mode most
// often used.
strict strictness = iota
// Like strict, but the root types specifically are compared as if they
// were not named. analyzer.ReduceToBase() is used to accomplish this.
// Assignment to private/restricted types is not allowed.
weak
// Full structural equivalence, and named types are always reduced.
// Assignment to private/restricted types is not allowed.
structural
// Data of the source type must be convert-able to the destination type.
// This is used in value casts. Assignment to private/restricted types
// is not allowed.
coerce
// All assignment rules are ignored. This is only used in bit casts.
force
)
// canAssign takes in an analyzed destination type and an analyzed source type,
// and determines whether source can be assigned to destination.
func (this *Tree) canAssign (
pos errors.Position,
// assuming both are analyzed already
destination entity.Type,
mode strictness,
source entity.Type,
) error {
fail := func () error {
switch mode {
case strict:
return errors.Errorf (
pos, "expected %v",
entity.FormatType(destination))
case weak:
return errors.Errorf (
pos, "cannot use %v as %v",
entity.FormatType(source),
entity.FormatType(destination))
case structural:
return errors.Errorf (
pos, "cannot convert from %v to %v",
entity.FormatType(source),
entity.FormatType(destination))
default:
panic(fmt.Sprint (
"BUG: analyzer doesnt know about mode", mode))
}
}
// check access permissions
if destination != nil && source != nil &&
mode != strict && mode != force {
err := this.typeRestricted(pos, destination)
if err != nil { return err }
err = this.typeRestricted(pos, source)
if err != nil { return err }
}
// short circuit if force is selected
if mode == force { return nil }
// check for coerce strictness
if mode == coerce {
return this.canAssignCoerce(pos, destination, source)
}
// do structural equivalence if structural is selected
if mode == structural {
if this.areStructurallyEquivalent(destination, source) {
return nil
} else {
return fail()
}
}
// first, check if this is interface assignment
if destination, ok := this.isInterface(destination); ok {
return this.canAssignInterface(pos, destination, source)
}
// then, check for array to slice assignment
if destination, ok := ReduceToBase(destination).(*entity.TypeSlice); ok {
if source, ok := ReduceToBase(source). (*entity.TypeArray); ok {
return this.canAssignSliceArray(pos, destination, source)
}}
// if weak, only compare the first base types
if mode == weak {
destination = ReduceToBase(destination)
source = ReduceToBase(source)
}
switch destination := destination.(type) {
// no type
case nil:
return nil
// named type
case *entity.TypeNamed:
if source, ok := source.(*entity.TypeNamed); ok {
if destination.Name == source.Name {
return nil
}
}
// composite base types
case *entity.TypePointer:
if source, ok := source.(*entity.TypePointer); ok {
mode := mode
if mode > structural { mode = structural }
return this.canAssign (
pos, destination.Referenced,
mode, source.Referenced)
}
case *entity.TypeSlice:
if source, ok := source.(*entity.TypeSlice); ok {
mode := mode
if mode > structural { mode = structural }
return this.canAssign (
pos, destination.Element,
mode, source.Element)
}
case *entity.TypeArray:
if source, ok := source.(*entity.TypeArray); ok {
mode := mode
if mode > structural { mode = structural }
return this.canAssign (
pos, destination.Element,
mode, source.Element)
}
// other base type
case *entity.TypeStruct,
*entity.TypeInt,
*entity.TypeFloat,
*entity.TypeWord:
if destination.Equals(source) {
return nil
}
default: panic(fmt.Sprint("BUG: analyzer doesnt know about type", destination))
}
return fail()
}
func TestAssignLiteralErrIntegerFloat (test *testing.T) {
testStringErr (test,
"expected integer", 3, 10,
`
[main] = {
x:Int = 5.5
}
`)
// canAssignCoerce determines if data of an analyzed source type can be
// converted into data of an analyzed destination type.
func (this *Tree) canAssignCoerce (
pos errors.Position,
destination entity.Type,
source entity.Type,
) error {
fail := func () error {
return errors.Errorf (
pos, "cannot convert from %v to %v",
entity.FormatType(source),
entity.FormatType(destination))
}
destination = ReduceToBase(destination)
source = ReduceToBase(source)
switch destination := destination.(type) {
// no type
case nil:
return nil
// base type
case *entity.TypeInt,
*entity.TypeFloat,
*entity.TypeWord:
switch source.(type) {
case *entity.TypeInt,
*entity.TypeFloat,
*entity.TypeWord:
// integers, floats, and words may all be assigned to
// eachother
return nil
default:
return fail()
}
case *entity.TypePointer:
switch source := source.(type) {
case *entity.TypeSlice:
// a slice may be assigned to a pointer if its element
// type can be assigned (structural) to the pointer's
// referenced type
err := this.canAssign (
pos, destination.Referenced,
structural, source.Element)
if err != nil {
return fail()
}
default:
// a pointer may be assigned to another pointer if they
// are structurally equivalent
if !this.areStructurallyEquivalent(destination, source) {
return fail()
}
}
case *entity.TypeSlice,
*entity.TypeArray,
*entity.TypeStruct:
// composite types may be assigned to eachother if they are
// structurally equivalent
if !this.areStructurallyEquivalent(destination, source) {
return fail()
}
default: fail()
}
return nil
}
func TestAssignLiteralErrIntegerStruct (test *testing.T) {
testStringErr (test,
"expected integer", 3, 10,
`
[main] = {
x:Int = (x: 5)
}
`)
// canAssignSliceArray takes in an analyzed slice type and an analyzed array
// type, and determines whether the array can be assigned to the slice.
func (this *Tree) canAssignSliceArray (
pos errors.Position,
destination *entity.TypeSlice,
source *entity.TypeArray,
) error {
err := this.canAssign(pos, destination.Element, strict, source.Element)
if err != nil {
return errors.Errorf (
pos, "expected %v",
entity.FormatType(destination))
} else {
return nil
}
}
func TestAssignLiteralErrIntegerArray (test *testing.T) {
testStringErr (test,
"expected integer", 3, 10,
`
[main] = {
x:Int = (* 5)
}
`)
// canAssignInterface takes in an analyzed interface destination type and an
// analyzed source type, and determines whether source can be assigned to
// destination.
func (this *Tree) canAssignInterface (
pos errors.Position,
destination *entity.TypeInterface,
source entity.Type,
) error {
for name, behavior := range destination.BehaviorMap {
// get method
method, err := this.analyzeMethodOrBehavior (
pos, source, name)
if err != nil { return err }
// extract signature
var signature *entity.Signature
switch method.(type) {
case *entity.Signature:
signature = method.(*entity.Signature)
case *entity.Method:
signature = method.(*entity.Method).Signature
default:
panic(fmt.Sprint (
"Tree.analyzeMethodOrBehavior returned",
method))
}
// check equivalence
if !signature.Equals(behavior) {
return errors.Errorf (
pos, "%v has wrong signature for method %v",
source, name)
}
}
return nil
}
func TestAssignLiteralErrIntegerOverflow (test *testing.T) {
testStringErr (test,
"expected smaller number", 3, 11,
`
[main] = {
x:Byte = 1000
}
`)
// areStructurallyEquivalent tests whether two types are structurally equivalent
// to eachother.
func (this *Tree) areStructurallyEquivalent (left, right entity.Type) bool {
left = ReduceToBase(left)
right = ReduceToBase(right)
switch left.(type) {
case nil: return false
// composite
case *entity.TypePointer:
left := left.(*entity.TypePointer)
right, ok := right.(*entity.TypePointer)
if !ok { return false }
return this.areStructurallyEquivalent(left.Referenced, right.Referenced)
case *entity.TypeSlice:
left := left.(*entity.TypeSlice)
right, ok := right.(*entity.TypeSlice)
if !ok { return false }
return this.areStructurallyEquivalent(left.Element, right.Element)
case *entity.TypeArray:
left := left.(*entity.TypeArray)
right, ok := right.(*entity.TypeArray)
if !ok { return false }
return left.Length == right.Length &&
this.areStructurallyEquivalent(left.Element, right.Element)
case *entity.TypeStruct:
left := left.(*entity.TypeStruct)
right, ok := right.(*entity.TypeStruct)
if !ok { return false }
if len(left.MemberMap) != len(right.MemberMap) { return false }
for name, member := range left.MemberMap {
other, ok := right.MemberMap[name]
if !ok { return false }
if !this.areStructurallyEquivalent(member.Type(), other.Type()) {
return false
}
}
// terminals
case *entity.TypeInt,
*entity.TypeFloat,
*entity.TypeWord:
return left.Equals(right)
default: panic(fmt.Sprint("BUG: analyzer doesnt know about type", left))
}
return false
}
func TestAssignLiteralErrArrayInt (test *testing.T) {
testStringErr (test,
"expected array", 3, 11,
`
[main] = {
x:3:Int = 5
}
`)
// isLocationExpression returns whether or not an expression is a valid location
// expression.
func (this *Tree) isLocationExpression (expression entity.Expression) error {
cannot := func (pos errors.Position, kind string) error {
return errors.Errorf(pos, "cannot assign to %s", kind)
}
switch expression := expression.(type) {
case *entity.Variable:
return nil
case *entity.Declaration:
return nil
case *entity.Call:
return cannot(expression.Position, "function call")
case *entity.MethodCall:
return cannot(expression.Position, "method call")
case *entity.Subscript:
return this.isLocationExpression(expression.Slice)
case *entity.Slice:
return cannot(expression.Position, "slice operation")
case *entity.Dereference:
return this.isLocationExpression(expression.Pointer)
case *entity.Reference:
return cannot(expression.Position, "reference operation")
case *entity.ValueCast:
return cannot(expression.Position, "value cast")
case *entity.BitCast:
return cannot(expression.Position, "bit cast")
case *entity.Operation:
return cannot(expression.Position, fmt.Sprintf (
"cannot assign to %v operation",
expression.Operator))
case *entity.Block:
return cannot(expression.Position, "block")
case *entity.MemberAccess:
return this.isLocationExpression (
expression.Source)
case *entity.IfElse:
return cannot(expression.Position, "if/else")
case *entity.Loop:
return cannot(expression.Position, "loop")
case *entity.Break:
return cannot(expression.Position, "break statement")
case *entity.Return:
return cannot(expression.Position, "return statement")
case *entity.LiteralInt:
return cannot(expression.Position, "integer literal")
case *entity.LiteralFloat:
return cannot(expression.Position, "float literal")
case *entity.LiteralString:
return cannot(expression.Position, "string literal")
case *entity.LiteralArray:
return cannot(expression.Position, "array literal")
case *entity.LiteralStruct:
return cannot(expression.Position, "struct literal")
case *entity.LiteralBoolean:
return cannot(expression.Position, "boolean literal")
case *entity.LiteralNil:
return cannot(expression.Position, "nil literal")
default:
panic(fmt.Sprint (
"BUG: analyzer doesnt know about expression",
expression))
}
}
func TestAssignLiteralErrArrayWrongLength (test *testing.T) {
testStringErr (test,
"expected 3 elements", 3, 11,
`
[main] = {
x:3:Int = (* 1 2 3 4)
}
`)
// ReduceToBase takes in an analyzed type and reduces it to its first non-name
// type.
func ReduceToBase (ty entity.Type) entity.Type {
if namedty, ok := ty.(*entity.TypeNamed); ok {
return ReduceToBase(namedty.Type)
} else {
return ty
}
}
func TestAssignLiteralErrArrayWrongType (test *testing.T) {
testStringErr (test,
"expected integer", 3, 19,
`
[main] = {
x:3:Int = (* 1 2 3.3 4)
}
`)
// isInterface takes in an analyzed type and returns true and an interface if
// that type refers to an interface. If not, it returns nil, false.
func (this *Tree) isInterface (ty entity.Type) (*entity.TypeInterface, bool) {
ty = ReduceToBase(ty)
switch ty.(type) {
case *entity.TypeInterface: return ty.(*entity.TypeInterface), true
default: return nil, false
}
}
func TestAssignLiteralErrSliceWrongType (test *testing.T) {
testStringErr (test,
"expected integer", 3, 19,
`
[main] = {
x:*:Int = (* 1 2 3.3 4)
}
`)
// isNumeric returns whether or not the specified type is a number.
func isNumeric (ty entity.Type) bool {
ty = ReduceToBase(ty)
switch ty.(type) {
case *entity.TypeInt, *entity.TypeFloat, *entity.TypeWord: return true
default: return false
}
}
func TestAssignLiteralErrStructWrongType (test *testing.T) {
testStringErr (test,
"expected integer", 3, 26,
`
[main] = {
x:3:(x:Int y:Int) = (x: 5.5 y: 3)
}
`)
}
func TestAssignLiteralErrStructInteger (test *testing.T) {
testStringErr (test,
"expected struct", 3, 26,
`
[main] = {
x:3:(x:Int y:Int) = 5
}
`)
// isInteger returns whether or not the specified type is an integer.
func isInteger (ty entity.Type) bool {
switch ReduceToBase(ty).(type) {
case *entity.TypeInt, *entity.TypeWord: return true
default: return false
}
}
func TestAssignLiteralErrInterfaceInt (test *testing.T) {
testStringErr (test,
"cannot assign literal to interface", 6, 2,
`
Bird: ([fly distance:F64] [land])
[main] = {
b:Bird = 5
}
`)
// isOrdered returns whether or not the values of the specified type can be
// ordered.
func isOrdered (ty entity.Type) bool {
return isNumeric(ty)
}
func TestAssignLiteralErrInterfaceFloat (test *testing.T) {
testStringErr (test,
"cannot assign literal to interface", 6, 2,
`
Bird: ([fly distance:F64] [land])
[main] = {
b:Bird = 5.5
}
`)
// isBoolean returns whether or not the specified type is a boolean.
func isBoolean (ty entity.Type) bool {
for {
named, ok := ty.(*entity.TypeNamed)
if !ok { return false }
if named.Name == "Bool" { return true }
}
}
func TestAssignLiteralErrInterfaceArray (test *testing.T) {
testStringErr (test,
"cannot assign literal to interface", 6, 2,
`
Bird: ([fly distance:F64] [land])
[main] = {
b:Bird = (* 1 2 3 4)
}
`)
// isFloat returns whether or not the specified type is a float.
func isFloat (ty entity.Type) bool {
switch ReduceToBase(ty).(type) {
case *entity.TypeFloat: return true
default: return false
}
}
func TestAssignLiteralErrInterfaceStruct (test *testing.T) {
testStringErr (test,
"cannot assign literal to interface", 6, 2,
`
Bird: ([fly distance:F64] [land])
BlueJay: Int
BlueJay::[land] = { }
[main] = {
b:Bird = (x: 5 y: 6)
}
`)
// IsUnsigned returns whether or not the specified type is an unsigned integer.
func IsUnsigned (ty entity.Type) bool {
switch ty := ReduceToBase(ty).(type) {
case *entity.TypeInt: return !ty.Signed
case *entity.TypeWord: return !ty.Signed
default: return false
}
}
func TestAssignLiteral (test *testing.T) {
testString (test,
`
[main] = {
a:F64 = 5.3
b:F32 = 5.3
c:F64 = -5
d:F32 = -5
e:Byte = 64
z:UInt = 5
x:Int = -5
arr:4:2:Int = (*
(* 1 2)
(* 3 4)
(* 5 6)
(* 7 8))
slice:*:Int = (* 3 1 2 3)
struct:(x:Int y:Int) = (
x: 9
y: 10)
}
`)
}
func TestAssignInterfaceErrBadSignature (test *testing.T) {
testStringErr (test,
"BlueJay has wrong signature for method fly", 7, 2,
`
Bird: ([fly distance:F64] [land])
BlueJay: Int
BlueJay::[fly] = { }
BlueJay::[land] = { }
[main] = {
b:Bird = [@a:BlueJay]
}
`)
}
func TestAssignInterface (test *testing.T) {
testString (test,
`
Bird: ([fly distance:F64] [land])
BlueJay: Int
BlueJay::[fly distance:F64] = { }
BlueJay::[fly land] = { }
[main] = {
a:BlueJay
b:Bird = [@a]
}
`)
}
func TestCastErrIntPointer (test *testing.T) {
testStringErr (test,
"cannot convert from *Int to Int", 2, 14,
`
[main]:Int = [~ [@ a:Int] Int]
`)
}
func TestCastErrIntStruct (test *testing.T) {
testStringErr (test,
"cannot convert from (x:Int y:Int) to Int", 2, 14,
`
[main]:Int = [~ a:(x:Int y:Int) Int]
`)
}
func TestCastErrIntArray (test *testing.T) {
testStringErr (test,
"cannot convert from 5:Int to Int", 2, 14,
`
[main]:Int = [~ a:5:Int Int]
`)
}
func TestCastErrIntSlice (test *testing.T) {
testStringErr (test,
"cannot convert from *:Int to Int", 2, 14,
`
[main]:Int = [~ a:*:Int Int]
`)
}
func TestCastErrPointerInt (test *testing.T) {
testStringErr (test,
"cannot convert from Int to *Int", 2, 15,
`
[main]:*Int = [~ [@ a:Int] *Int]
`)
}
func TestCastErrStructInt (test *testing.T) {
testStringErr (test,
"cannot convert from Int to (x:Int y:Int)", 2, 24,
`
[main]:(x:Int y:Int) = [~ a:Int (x:Int y:Int)]
`)
}
func TestCastErrArrayInt (test *testing.T) {
testStringErr (test,
"cannot convert from Int to 5:Int", 2, 13,
`
[main]:Int = [~ a:5:Int Int]
`)
}
func TestCastErrSliceInt (test *testing.T) {
testStringErr (test,
"cannot convert from Int to *:Int", 2, 16,
`
[main]:*:Int = [~ a:Int *:Int]
`)
}
func TestCast (test *testing.T) {
testString (test,
`
Bird: ([fly distance:F64] [land])
BlueJay: Int
BlueJay::[fly distance:F64] = { }
BlueJay::[fly land] = { }
IntDerived: Int
[main] = {
a:IntDerived = 5
b:Int [~ [~ [~ a Byte] F64] Int]
c:Int [~~ [~~ [~~ a Byte] F64] Int]
d:(x:Int y:Int) = (x: 1 y: 2)
e:(z:Int a:Int) = [~~ d (z:Int a:Int)]
f:Bird = [@ [~~ 0 BlueJay]]
g:*:Int = (~ h:5:Int *:int)
}
`)
}
// TODO: complete and test error cases
func TestPropagateAssignRules (test *testing.T) {
testString (test,
`
[f]:Byte = 5
A:Int
A::[g]:Int = 2
B:([g]:Int)
[main] = {
a:Int
b:Int = a
c:Int = d:Int
d:Int = { a:F64 b }
e:Byte = [f]
g:(x:Int y:(w:F64 z:F64)) = (x: 1 y: (w: 1.2 z: 78.5))
h:F64 = g.x.z
i:A
j:Int = [i::g]
k:B = i
l:Int = [k::g]
m:5:Int = (* 0 1 2 3 4)
n:Int = [. m 3]
}
`)
// inRange returns whether the specified value can fit within the given integer
// type.
func inRange (ty entity.Type, value int64) bool {
base := ReduceToBase(ty)
switch base.(type) {
case *entity.TypeInt:
base := base.(*entity.TypeInt)
if base.Signed {
return value > integer.SignedMin(base.Width) &&
value < integer.SignedMax(base.Width)
} else {
return value >= 0 &&
uint64(value) < integer.UnsignedMax(base.Width)
}
case *entity.TypeWord:
base := base.(*entity.TypeWord)
if base.Signed {
return value > math.MinInt && value < math.MaxInt
} else {
return value >= 0 && uint64(value) < math.MaxUint
}
default: return false
}
}

289
analyzer/assignment_test.go Normal file
View File

@ -0,0 +1,289 @@
package analyzer
import "testing"
func TestAssignmentLiteralErrUnsignedNegative (test *testing.T) {
testStringErr (test,
"integer literal out of range for type UInt", 3, 11,
`
[main] = {
x:UInt = -5
}
`)
}
func TestAssignmentLiteralErrIntegerFloat (test *testing.T) {
testStringErr (test,
"cannot use float literal as Int", 3, 10,
`
[main] = {
x:Int = 5.5
}
`)
}
func TestAssignmentLiteralErrIntegerStruct (test *testing.T) {
testStringErr (test,
"cannot use struct literal as Int", 3, 10,
`
[main] = {
x:Int = (. x: 5)
}
`)
}
func TestAssignmentLiteralErrIntegerArray (test *testing.T) {
testStringErr (test,
"cannot use array literal as Int", 3, 10,
`
[main] = {
x:Int = (* 5)
}
`)
}
func TestAssignmentLiteralErrIntegerOverflow (test *testing.T) {
testStringErr (test,
"integer literal out of range for type Byte", 3, 11,
`
[main] = {
x:Byte = 1000
}
`)
}
func TestAssignmentLiteralErrArrayInt (test *testing.T) {
testStringErr (test,
"cannot use integer literal as 3:Int", 3, 12,
`
[main] = {
x:3:Int = 5
}
`)
}
func TestAssignmentLiteralErrArrayWrongLength (test *testing.T) {
testStringErr (test,
"expected 3 elements or less", 3, 12,
`
[main] = {
x:3:Int = (* 1 2 3 4)
}
`)
}
func TestAssignmentLiteralErrArrayWrongType (test *testing.T) {
testStringErr (test,
"cannot use float literal as Int", 3, 17,
`
[main] = {
x:3:Int = (* 1 3.3 4)
}
`)
}
func TestAssignmentLiteralErrSliceWrongType (test *testing.T) {
testStringErr (test,
"cannot use float literal as Int", 3, 17,
`
[main] = {
x:*:Int = (* 1 3.3 4)
}
`)
}
func TestAssignmentLiteralErrStructWrongType (test *testing.T) {
testStringErr (test,
"cannot use float literal as Int", 3, 28,
`
[main] = {
x:(. x:Int y:Int) = (. x: 5.5 y: 3)
}
`)
}
func TestAssignmentLiteralErrStructInteger (test *testing.T) {
testStringErr (test,
"cannot use integer literal as (. x:Int y:Int)", 3, 22,
`
[main] = {
x:(. x:Int y:Int) = 5
}
`)
}
func TestAssignmentLiteralErrInterfaceInt (test *testing.T) {
testStringErr (test,
"cannot use integer literal as Bird", 4, 11,
`
Bird: (~ [fly distance:F64] [land])
[main] = {
b:Bird = 5
}
`)
}
func TestAssignmentLiteralErrInterfaceFloat (test *testing.T) {
testStringErr (test,
"cannot use float literal as Bird", 4, 11,
`
Bird: (~ [fly distance:F64] [land])
[main] = {
b:Bird = 5.5
}
`)
}
func TestAssignmentLiteralErrInterfaceArray (test *testing.T) {
testStringErr (test,
"cannot use array literal as Bird", 4, 11,
`
Bird: (~ [fly distance:F64] [land])
[main] = {
b:Bird = (* 1 2 3 4)
}
`)
}
func TestAssignmentLiteralErrInterfaceStruct (test *testing.T) {
testStringErr (test,
"cannot use struct literal as Bird", 4, 11,
`
Bird: (~ [fly distance:F64] [land])
[main] = {
b:Bird = (. x: 5 y: 6)
}
`)
}
func TestAssignmentLiteralErrDerefByteFloat (test *testing.T) {
testStringErr (test,
"cannot use float literal as Byte", 4, 17,
`
[main] = {
buffer:8:Byte
[. buffer 7] = 0.0
}
`)
}
func TestAssignmentLiteral (test *testing.T) {
testString (test,
`
[main] = {
a:F64 = 5.3
b:F32 = 5.3
c:F64 = -5
d:F32 = -5
e:Byte = 64
z:UInt = 5
x:Int = -5
arr:4:2:Int = (*
(* 1 2)
(* 3 4)
(* 5 6)
(* 7 8))
slice:*:Int = (* 3 1 2 3)
struct:(. x:Int y:Int) = (.
x: 9
y: 10)
[.[.arr 1] 0] = 6
[.slice 4] = 5
}
`)
}
func TestAssignmentInterfaceErrBadSignature (test *testing.T) {
testStringErr (test,
"BlueJay has wrong signature for method fly", 7, 11,
`
Bird: (~ [fly distance:F64] [land])
BlueJay: Int
BlueJay.[fly] = { }
BlueJay.[land] = { }
[main] = {
b:Bird = a:BlueJay
}
`)
}
func TestAssignmentInterfaceErrMissingMethod (test *testing.T) {
testStringErr (test,
"no method named fly defined on this type", 6, 11,
`
Bird: (~ [fly distance:F64] [land])
BlueJay: Int
BlueJay.[land] = { }
[main] = {
b:Bird = a:BlueJay
}
`)
}
func TestAssignmentInterfaceErrBadLayer (test *testing.T) {
testStringErr (test,
"no method named fly defined on this type", 7, 11,
`
Bird: (~ [fly distance:F64])
BlueJay: Int
BlueJay.[fly distance:F64] = { }
BlueJayRef: *BlueJay
[main] = {
b:Bird = a:BlueJayRef
}
`)
}
func TestAssignmentInterface (test *testing.T) {
testString (test,
`
Bird: (~ [fly distance:F64] [land])
BlueJay: Int
BlueJay.[fly distance:F64] = { }
BlueJay.[land] = { }
[main] = {
b:Bird = a:BlueJay
ref:*Bird = [@ a]
c:Bird = ref
}
`)
}
func TestAssignmentErrByteSliceString (test *testing.T) {
testStringErr (test,
"expected *:Byte", 4, 13,
`
[main] = {
a:String = 'hello'
b:*:Byte = a
}
`)
}
// TODO: complete and test error cases
func TestAssignmentPropagateRules (test *testing.T) {
testString (test,
`
[f]:Byte = 5
A:Int
A.[g]:Int = 2
B:(~ [g]:Int)
[main] = {
a:Int
b:Int = a
c:Int = d:Int
d = { a:F64 b }
e:Byte = [f]
g:(. x:Int y:(. w:F64 z:F64)) = (. x: 1 y: (. w: 1.2 z: 78.5))
gx:(. w:F64 z:F64) = g.y
h:F64 = gx.z
i:A
j:Int = i.[g]
k:B = i
l:Int = k.[g]
m:5:Int = (* 0 1 2 3 4)
n:Int = [. m 3]
o:*:Int = m
}
`)
}

51
analyzer/builtin.go Normal file
View File

@ -0,0 +1,51 @@
package analyzer
import "git.tebibyte.media/fspl/fspl/entity"
var primitiveTypes = map[string] entity.Type {
"Int": &entity.TypeWord { Acc: entity.AccessPublic, Signed: true },
"UInt": &entity.TypeWord { Acc: entity.AccessPublic, },
"I8": &entity.TypeInt { Acc: entity.AccessPublic, Signed: true, Width: 8 },
"I16": &entity.TypeInt { Acc: entity.AccessPublic, Signed: true, Width: 16 },
"I32": &entity.TypeInt { Acc: entity.AccessPublic, Signed: true, Width: 32 },
"I64": &entity.TypeInt { Acc: entity.AccessPublic, Signed: true, Width: 64 },
"U8": &entity.TypeInt { Acc: entity.AccessPublic, Signed: false, Width: 8 },
"U16": &entity.TypeInt { Acc: entity.AccessPublic, Signed: false, Width: 16 },
"U32": &entity.TypeInt { Acc: entity.AccessPublic, Signed: false, Width: 32 },
"U64": &entity.TypeInt { Acc: entity.AccessPublic, Signed: false, Width: 64 },
"F32": &entity.TypeFloat { Acc: entity.AccessPublic, Width: 32 },
"F64": &entity.TypeFloat { Acc: entity.AccessPublic, Width: 64 },
}
var builtinTypes = map[string] entity.Type { }
func builtinType (name string) *entity.TypeNamed {
ty, ok := builtinTypes[name]
if !ok { panic("BUG: compiler tried to reference missing builtin " + name) }
return &entity.TypeNamed {
Name: name,
Type: ty,
Acc: entity.AccessPublic,
}
}
func primitiveType (name string) *entity.TypeNamed {
ty, ok := primitiveTypes[name]
if !ok { panic("BUG: compiler tried to reference missing primitive " + name) }
return &entity.TypeNamed {
Name: name,
Type: ty,
Acc: entity.AccessPublic,
}
}
func init () {
builtinTypes["Index"] = &entity.TypeWord { Acc: entity.AccessPublic }
builtinTypes["Byte"] = &entity.TypeInt { Acc: entity.AccessPublic, Signed: false, Width: 8 }
builtinTypes["Bool"] = &entity.TypeInt { Acc: entity.AccessPublic, Signed: false, Width: 8 }
builtinTypes["Rune"] = &entity.TypeInt { Acc: entity.AccessPublic, Signed: false, Width: 32 }
builtinTypes["String"] = &entity.TypeSlice {
Acc: entity.AccessPublic,
Element: &entity.TypeInt { Acc: entity.AccessPublic, Signed: false, Width: 8 },
}
}

122
analyzer/cast_test.go Normal file
View File

@ -0,0 +1,122 @@
package analyzer
import "testing"
func TestCastErrIntPointer (test *testing.T) {
testStringErr (test,
"cannot convert from *Int to Int", 4, 9,
`
[main]:Int = {
a:*Int
[~ Int a]
}
`)
}
func TestCastErrIntStruct (test *testing.T) {
testStringErr (test,
"cannot convert from (. x:Int y:Int) to Int", 2, 21,
`
[main]:Int = [~ Int a:(. x:Int y:Int)]
`)
}
func TestCastErrIntArray (test *testing.T) {
testStringErr (test,
"cannot convert from 5:Int to Int", 2, 21,
`
[main]:Int = [~ Int a:5:Int]
`)
}
func TestCastErrIntSlice (test *testing.T) {
testStringErr (test,
"cannot convert from *:Int to Int", 2, 21,
`
[main]:Int = [~ Int a:*:Int]
`)
}
func TestCastErrPointerInt (test *testing.T) {
testStringErr (test,
"cannot convert from Int to *Int", 2, 23,
`
[main]:*Int = [~ *Int a:Int]
`)
}
func TestCastErrStructInt (test *testing.T) {
testStringErr (test,
"cannot convert from Int to (. x:Int y:Int)", 2, 45,
`
[main]:(. x:Int y:Int) = [~ (. x:Int y:Int) a:Int]
`)
}
func TestCastErrArrayInt (test *testing.T) {
testStringErr (test,
"cannot convert from 5:Int to Int", 2, 21,
`
[main]:Int = [~ Int a:5:Int]
`)
}
func TestCastErrSliceInt (test *testing.T) {
testStringErr (test,
"cannot convert from Int to *:Int", 2, 25,
`
[main]:*:Int = [~ *:Int a:Int]
`)
}
func TestCast (test *testing.T) {
testString (test,
`
Bird: (~ [fly distance:F64] [land])
BlueJay: Int
BlueJay.[fly distance:F64] = { }
BlueJay.[land] = { }
IntDerived: Int
[main] = {
a:IntDerived = 5
b:Int [~ Int [~ F64 [~ Byte a]]]
c:Int [~~ Int [~~ F64 [~~ Byte a]]]
d:(. x:Int y:Int) = (. x: 1 y: 2)
e:(. z:Int a:Int) = [~~ (. z:Int a:Int) d]
f:Bird = [~~ BlueJay 0]
g:String = 'hello'
h:*:Byte = [~ *:Byte g]
}
`)
}
func TestBitCastPointer (test *testing.T) {
testString (test,
`
Struct: (. x:Int y:Int)
Array: 4:Int
[main] = {
ptr:*Byte
stc:Struct
arr:Array
str:String
idx:Index
; struct
ptr = [~~*Byte [~~Struct ptr]]
stc = [~~Struct [~~*Byte stc]]
; array
ptr = [~~*Byte [~~Array ptr]]
arr = [~~Array [~~*Byte arr]]
; slice
ptr = [~~*Byte [~~String ptr]]
str = [~~String [~~*Byte str]]
; int
ptr = [~~*Byte [~~Index ptr]]
idx = [~~Index [~~*Byte idx]]
; arithmetic
ptr = [~~*Byte [+ 1 [~~Index ptr]]]
}
`)
}

5
analyzer/doc.go Normal file
View File

@ -0,0 +1,5 @@
// Package analyzer implements the semantic analysis stage of the FSPL compiler.
// The analyzer takes in a well-formed abstract syntax tree, ensures its
// semantic correctness, and fills in the semantic information stored within
// the tree.
package analyzer

View File

@ -0,0 +1,73 @@
package analyzer
import "fmt"
import "git.tebibyte.media/fspl/fspl/entity"
func (this *Tree) analyzeExpression (
into entity.Type,
mode strictness,
expression entity.Expression,
) (
entity.Expression,
error,
) {
switch expression := expression.(type) {
case *entity.Assignment:
return this.analyzeAssignment(expression)
case *entity.Variable:
return this.analyzeVariable(into, mode, expression)
case *entity.Declaration:
return this.analyzeDeclaration(into, mode, expression)
case *entity.Call:
return this.analyzeCall(into, mode, expression)
case *entity.MethodCall:
return this.analyzeMethodCall(into, mode, expression)
case *entity.Subscript:
return this.analyzeSubscript(into, mode, expression)
case *entity.Slice:
return this.analyzeSlice(into, mode, expression)
case *entity.Length:
return this.analyzeLength(into, mode, expression)
case *entity.Dereference:
return this.analyzeDereference(into, mode, expression)
case *entity.Reference:
return this.analyzeReference(into, mode, expression)
case *entity.ValueCast:
return this.analyzeValueCast(into, mode, expression)
case *entity.BitCast:
return this.analyzeBitCast(into, mode, expression)
case *entity.Operation:
return this.analyzeOperation(into, mode, expression)
case *entity.Block:
return this.analyzeBlock(into, mode, expression)
case *entity.MemberAccess:
return this.analyzeMemberAccess(into, mode, expression)
case *entity.IfElse:
return this.analyzeIfElse(into, mode, expression)
case *entity.Loop:
return this.analyzeLoop(into, mode, expression)
case *entity.Break:
return this.analyzeBreak(into, mode, expression)
case *entity.Return:
return this.analyzeReturn(into, mode, expression)
case *entity.LiteralInt:
return this.analyzeLiteralInt(into, mode, expression)
case *entity.LiteralFloat:
return this.analyzeLiteralFloat(into, mode, expression)
case *entity.LiteralArray:
return this.analyzeLiteralArray(into, mode, expression)
case *entity.LiteralString:
return this.analyzeLiteralString(into, mode, expression)
case *entity.LiteralStruct:
return this.analyzeLiteralStruct(into, mode, expression)
case *entity.LiteralBoolean:
return this.analyzeLiteralBoolean(into, mode, expression)
case *entity.LiteralNil:
return this.analyzeLiteralNil(into, mode, expression)
default:
panic(fmt.Sprintf (
"BUG: analyzer doesnt know about expression %v, ty: %T",
expression, expression))
}
}

805
analyzer/expression.go Normal file
View File

@ -0,0 +1,805 @@
package analyzer
import "fmt"
import "git.tebibyte.media/fspl/fspl/errors"
import "git.tebibyte.media/fspl/fspl/entity"
// All expression analysis routines must take in the type they are being
// assigned to and return an error if they can't be assigned to it. if the type
// is nil, the expression must ignore it unless it can do upwards type
// inference.
func (this *Tree) analyzeAssignment (
assignment *entity.Assignment,
) (
entity.Expression,
error,
) {
// analyze location
location, err := this.analyzeExpression(nil, strict, assignment.Location)
if err != nil { return nil, err }
assignment.Location = location
// ensure location is location expression
err = this.isLocationExpression(location)
if err != nil { return nil, err }
// analyze value
value, err := this.analyzeExpression(location.Type(), strict, assignment.Value)
if err != nil { return nil, err }
assignment.Value = value
return assignment, nil
}
func (this *Tree) analyzeVariable (
into entity.Type,
mode strictness,
variable *entity.Variable,
) (
entity.Expression,
error,
) {
declaration := this.variable(variable.Name)
if declaration == nil {
return nil, errors.Errorf (
variable.Position, "no variable named %s",
variable.Name)
}
err := this.canAssign(variable.Position, into, mode, declaration.Type())
if err != nil { return nil, err }
variable.Declaration = declaration
return variable, nil
}
func (this *Tree) analyzeDeclaration (
into entity.Type,
mode strictness,
declaration *entity.Declaration,
) (
entity.Expression,
error,
) {
scope, _ := this.topScope()
existing := scope.Variable(declaration.Name)
if existing != nil {
return nil, errors.Errorf (
declaration.Position,
"%s already declared in block at %v",
declaration.Name, existing.Position)
}
ty, err := this.analyzeType(declaration.Ty, false)
declaration.Ty = ty
if err != nil { return nil, err }
err = this.canAssign(declaration.Position, into, mode, declaration.Type())
if err != nil { return nil, err }
this.addVariable(declaration)
return declaration, nil
}
func (this *Tree) analyzeCall (
into entity.Type,
mode strictness,
call *entity.Call,
) (
entity.Expression,
error,
) {
// get function
unit, err := this.resolveNickname(call.Position, call.UnitNickname)
if err != nil { return nil, err }
function, err := this.analyzeFunction(call.Position, entity.Key {
Unit: unit,
Name: call.Name,
})
if err != nil { return nil, err }
call.Function = function
call.Unit = function.Unit
// check access permissions
if function.Acc == entity.AccessPrivate && function.Unit != this.unit {
return nil, errors.Errorf (
call.Position, "function %v::[%v] is private",
call.UnitNickname, call.Name)
}
// check return result
err = this.canAssign(call.Position, into, mode, function.Signature.Return)
if err != nil { return nil, err }
// check arg count
if len(call.Arguments) > len(function.Signature.Arguments) {
return nil, errors.Errorf (
call.Position, "too many arguments in call to %s",
call.Name)
} else if len(call.Arguments) < len(function.Signature.Arguments) {
return nil, errors.Errorf (
call.Position, "too few arguments in call to %s",
call.Name)
}
// check arg types
for index, argument := range call.Arguments {
signature := function.Signature
correct := signature.ArgumentMap[signature.ArgumentOrder[index]]
argument, err := this.analyzeExpression(correct.Type(), strict, argument)
if err != nil { return nil, err }
call.Arguments[index] = argument
}
return call, nil
}
func (this *Tree) analyzeMethodCall (
into entity.Type,
mode strictness,
call *entity.MethodCall,
) (
entity.Expression,
error,
) {
// get method
source, err := this.analyzeExpression(nil, strict, call.Source)
if err != nil { return nil, err }
method, err := this.analyzeMethodOrBehavior (
call.Position, source.Type(), call.Name)
if err != nil { return nil, err }
// extract signature
var signature *entity.Signature
switch method := method.(type) {
case *entity.Signature:
signature = method
call.Behavior = signature
// since this is a behavior, check access permissions of the
// interface
err = this.typeRestricted(call.Position, source.Type())
if err != nil { return nil, err }
case *entity.Method:
signature = method.Signature
call.Method = method
// since this is a method, check access permissions of the
// method
if method.Acc == entity.AccessPrivate && method.Unit != this.unit {
return nil, errors.Errorf (
call.Position, "method %v.[%v] is private",
method.TypeName, method.Signature.Name)
}
default:
panic(fmt.Sprint (
"Tree.analyzeMethodOrBehavior returned ",
method))
}
// check return result
err = this.canAssign(call.Position, into, mode, signature.Return)
if err != nil { return nil, err }
// check arg count
if len(call.Arguments) > len(signature.Arguments) {
return nil, errors.Errorf (
call.Position, "too many arguments in call to %s",
call.Name)
} else if len(call.Arguments) < len(signature.Arguments) {
return nil, errors.Errorf (
call.Position, "too few arguments in call to %s",
call.Name)
}
// check arg types
for index, argument := range call.Arguments {
correct := signature.ArgumentMap[signature.ArgumentOrder[index]]
argument, err := this.analyzeExpression(correct.Type(), strict, argument)
if err != nil { return nil, err }
call.Arguments[index] = argument
}
return call, nil
}
func (this *Tree) analyzeSubscript (
into entity.Type,
mode strictness,
subscript *entity.Subscript,
) (
entity.Expression,
error,
) {
slice, err := this.analyzeExpression (
&entity.TypeSlice {
Position: subscript.Position,
Element: into,
}, weak,
subscript.Slice)
if err != nil { return nil, err }
subscript.Slice = slice
subscript.Ty = into
// check permissions
err = this.typeRestricted(subscript.Position, slice.Type())
if err != nil { return nil, err }
offset, err := this.analyzeExpression (
builtinType("Index"), weak,
subscript.Offset)
if err != nil { return nil, err }
subscript.Offset = offset
var frailType bool
switch into := into.(type) {
case nil: frailType = true
case *entity.TypeSlice: frailType = into.Element == nil
}
if frailType {
ty := ReduceToBase(subscript.Slice.Type())
switch ty := ty.(type) {
case *entity.TypeSlice: subscript.Ty = ty.Element
case *entity.TypeArray: subscript.Ty = ty.Element
}
}
return subscript, nil
}
func (this *Tree) analyzeSlice (
into entity.Type,
mode strictness,
slice *entity.Slice,
) (
entity.Expression,
error,
) {
value, err := this.analyzeExpression(into, weak, slice.Slice)
if err != nil { return nil, err }
slice.Slice = value
// check permissions
err = this.typeRestricted(slice.Position, value.Type())
if err != nil { return nil, err }
if slice.Start != nil {
start, err := this.analyzeExpression (
builtinType("Index"), weak,
slice.Start)
if err != nil { return nil, err }
slice.Start = start
}
if slice.End != nil {
end, err := this.analyzeExpression (
builtinType("Index"), weak,
slice.End)
if err != nil { return nil, err }
slice.End = end
}
return slice, nil
}
func (this *Tree) analyzeLength (
into entity.Type,
mode strictness,
length *entity.Length,
) (
entity.Expression,
error,
) {
value, err := this.analyzeExpression(nil, strict, length.Slice)
if err != nil { return nil, err }
length.Slice = value
length.Ty = builtinType("Index")
return length, nil
}
func (this *Tree) analyzeDereference (
into entity.Type,
mode strictness,
dereference *entity.Dereference,
) (
entity.Expression,
error,
) {
pointer, err := this.analyzeExpression (
&entity.TypePointer {
Position: dereference.Position,
Referenced: into,
}, weak,
dereference.Pointer)
if err != nil { return nil, err }
dereference.Pointer = pointer
dereference.Ty = into
var frailType bool
switch into := into.(type) {
case nil: frailType = true
case *entity.TypePointer: frailType = into.Referenced == nil
}
if frailType {
ty := ReduceToBase(dereference.Pointer.Type())
switch ty := ty.(type) {
case *entity.TypePointer: dereference.Ty = ty.Referenced
}
}
return dereference, nil
}
func (this *Tree) analyzeReference (
into entity.Type,
mode strictness,
reference *entity.Reference,
) (
entity.Expression,
error,
) {
referenced, ok := into.(*entity.TypePointer)
if !ok {
return nil, errors.Errorf(reference.Position, "expected %v", into)
}
value, err := this.analyzeExpression (
referenced.Referenced, weak,
reference.Value)
if err != nil { return nil, err }
err = this.isLocationExpression(reference.Value)
if err != nil { return nil, err }
reference.Value = value
reference.Ty = referenced.Referenced
return reference, nil
}
func (this *Tree) analyzeValueCast (
into entity.Type,
mode strictness,
cast *entity.ValueCast,
) (
entity.Expression,
error,
) {
ty, err := this.analyzeType(cast.Ty, false)
if err != nil { return nil, err }
cast.Ty = ty
err = this.canAssign(cast.Position, into, mode, cast.Type())
if err != nil { return nil, err }
value, err := this.analyzeExpression(cast.Ty, coerce, cast.Value)
if err != nil { return nil, err }
cast.Value = value
return cast, nil
}
func (this *Tree) analyzeBitCast (
into entity.Type,
mode strictness,
cast *entity.BitCast,
) (
entity.Expression,
error,
) {
ty, err := this.analyzeType(cast.Ty, false)
if err != nil { return nil, err }
cast.Ty = ty
err = this.canAssign(cast.Position, into, mode, cast.Type())
if err != nil { return nil, err }
value, err := this.analyzeExpression(cast.Ty, force, cast.Value)
if err != nil { return nil, err }
cast.Value = value
return cast, nil
}
func (this *Tree) analyzeOperation (
into entity.Type,
mode strictness,
operation *entity.Operation,
) (
entity.Expression,
error,
) {
// check permissions
err := this.typeRestricted(operation.Position, into)
if err != nil { return nil, err }
intoUntrustworthy := into == nil || mode == force || mode == coerce
undefined := func (ty entity.Type) (entity.Expression, error) {
return nil, errors.Errorf (
operation.Position, "operator %v undefined for %v",
operation.Operator, entity.FormatType(ty))
}
wrongInto := func () (entity.Expression, error) {
return nil, errors.Errorf (
operation.Position, "expected %v",
entity.FormatType(into))
}
wrongArgCount := func () (entity.Expression, error) {
return nil, errors.Errorf (
operation.Position, "wrong argument count for %v",
operation.Operator)
}
determineArgumentType := func () (entity.Type, int, error) {
// find the first argument that has explicit type information.
boss := -1
var argumentType entity.Type
for index, argument := range operation.Arguments {
if argument.HasExplicitType() {
argument, err := this.analyzeExpression(nil, strict, argument)
if err != nil { return nil, -1, err }
boss = index
operation.Arguments[index] = argument
argumentType = argument.Type()
break
}
}
if argumentType == nil {
return nil, -1, errors.Errorf (
operation.Position,
"operation arguments have ambiguous type")
}
return argumentType, boss, err
}
nSameType := func (n int, constraint func (entity.Type) bool) (entity.Expression, error) {
if n > 0 {
if len(operation.Arguments) != n { return wrongArgCount() }
} else {
if len(operation.Arguments) < 1 { return wrongArgCount() }
}
n = len(operation.Arguments)
boss := -1
var argumentType entity.Type
if intoUntrustworthy {
// determine the type of all arguments (and the return
// type) using determineArgumentType
var err error
argumentType, boss, err = determineArgumentType()
if err != nil { return nil, err }
if !constraint(argumentType) { return undefined(argumentType) }
} else {
// into is trustworthy, so we don't need to determine
// anything
argumentType = into
}
if !constraint(argumentType) { return undefined(argumentType) }
operation.Ty = argumentType
for index, argument := range operation.Arguments {
if index == boss { continue }
argument, err := this.analyzeExpression (
operation.Ty, strict,
argument)
if err != nil { return nil, err }
operation.Arguments[index] = argument
}
return operation, nil
}
comparison := func (argConstraint func (entity.Type) bool) (entity.Expression, error) {
if len(operation.Arguments) < 2 { return wrongArgCount() }
if !isBoolean(into) && !intoUntrustworthy { return wrongInto() }
operation.Ty = builtinType("Bool")
// determine argument type
argumentType, boss, err := determineArgumentType()
if err != nil { return nil, err }
if !argConstraint(argumentType) { return undefined(argumentType) }
// analyze all remaining arguments
for index, argument := range operation.Arguments {
if index == boss { continue }
argument, err := this.analyzeExpression (
argumentType, strict, argument)
if err != nil { return nil, err }
operation.Arguments[index] = argument
}
return operation, nil
}
switch operation.Operator {
// math
case entity.OperatorAdd,
entity.OperatorSubtract,
entity.OperatorMultiply,
entity.OperatorDivide,
entity.OperatorIncrement,
entity.OperatorDecrement:
return nSameType(-1, isNumeric)
case entity.OperatorModulo:
return nSameType(2, isNumeric)
// logic
case entity.OperatorLogicalNot:
return nSameType(1, isBoolean)
case entity.OperatorLogicalOr,
entity.OperatorLogicalAnd,
entity.OperatorLogicalXor:
return nSameType(-1, isBoolean)
// bit manipulation
case entity.OperatorNot:
return nSameType(1, isInteger)
case entity.OperatorOr, entity.OperatorAnd, entity.OperatorXor:
return nSameType(-1, isInteger)
case entity.OperatorLeftShift, entity.OperatorRightShift:
if len(operation.Arguments) != 2 { return wrongArgCount() }
if !isInteger(into) { return wrongInto() }
arg, err := this.analyzeExpression(into, mode, operation.Arguments[0])
if err != nil { return nil, err }
operation.Arguments[0] = arg
operation.Ty = arg.Type()
offset, err := this.analyzeExpression (
builtinType("Index"), weak,
operation.Arguments[1])
if err != nil { return nil, err }
operation.Arguments[1] = offset
return operation, nil
// comparison
case entity.OperatorLess,
entity.OperatorGreater,
entity.OperatorLessEqual,
entity.OperatorGreaterEqual:
return comparison(isOrdered)
case entity.OperatorEqual:
return comparison(isOrdered)
default:
panic(fmt.Sprint (
"BUG: analyzer doesnt know about operator ",
operation.Operator))
}
}
func (this *Tree) analyzeBlock (
into entity.Type,
mode strictness,
block *entity.Block,
) (
entity.Expression,
error,
) {
this.pushScope(block)
defer this.popScope()
if len(block.Steps) == 0 && into != nil {
return nil, errors.Errorf (
block.Position, "block must have at least one statement")
}
final := len(block.Steps) - 1
for index, step := range block.Steps {
if index == final && into != nil {
expression, ok := step.(entity.Expression)
if !ok {
return nil, errors.Errorf (
block.Position, "expected expression")
}
step, err := this.analyzeExpression(into, strict, expression)
if err != nil { return nil, err }
block.Steps[index] = step
block.Ty = step.Type()
} else {
step, err := this.analyzeExpression(nil, strict, step)
if err != nil { return nil, err }
block.Steps[index] = step
}
}
return block, nil
}
func (this *Tree) analyzeMemberAccess (
into entity.Type,
mode strictness,
access *entity.MemberAccess,
) (
entity.Expression,
error,
) {
source, err := this.analyzeExpression(nil, strict, access.Source)
if err != nil { return nil, err }
// determine type with the members
var sourceType *entity.TypeStruct
var qualifiedSourceType entity.Type
switch sourceTypeAny := ReduceToBase(source.Type()).(type) {
case *entity.TypeStruct:
sourceType = sourceTypeAny
qualifiedSourceType = source.Type()
case *entity.TypePointer:
referenced, ok := ReduceToBase(sourceTypeAny.Referenced).(*entity.TypeStruct)
if !ok {
return nil, errors.Errorf (
access.Position, "cannot access members of %v",
source)
}
sourceType = referenced
qualifiedSourceType = sourceTypeAny.Referenced
default:
return nil, errors.Errorf (
access.Position, "cannot access members of %v",
source)
}
// check permissions
err = this.typeRestricted(access.Position, qualifiedSourceType)
if err != nil { return nil, err }
// get member
member, ok := sourceType.MemberMap[access.Member]
if !ok {
return nil, errors.Errorf (
access.Position, "no member %v",
access)
}
err = this.canAssign(access.Position, into, mode, member.Type())
if err != nil { return nil, err }
access.Ty = member.Type()
return access, nil
}
func (this *Tree) analyzeIfElse (
into entity.Type,
mode strictness,
ifelse *entity.IfElse,
) (
entity.Expression,
error,
) {
condition, err := this.analyzeExpression (
&entity.TypeNamed {
Name: "Bool",
Type: builtinType("Bool"),
},
weak, ifelse.Condition)
if err != nil { return nil, err }
ifelse.Condition = condition
trueBranch, err := this.analyzeExpression(into, strict, ifelse.True)
if err != nil { return nil, err }
ifelse.True = trueBranch
if ifelse.False == nil {
if into != nil {
return nil, errors.Errorf (
ifelse.Position,
"else case required when using value of if ")
}
} else {
falseBranch, err := this.analyzeExpression(into, strict, ifelse.False)
if err != nil { return nil, err }
ifelse.False = falseBranch
}
ifelse.Ty = into
return ifelse, nil
}
func (this *Tree) analyzeLoop (
into entity.Type,
mode strictness,
loop *entity.Loop,
) (
entity.Expression,
error,
) {
loop.Ty = into
this.pushLoop(loop)
defer this.popLoop()
body, err := this.analyzeExpression(nil, strict, loop.Body)
if err != nil { return nil, err }
loop.Body = body
return loop, nil
}
func (this *Tree) analyzeBreak (
into entity.Type,
mode strictness,
brk *entity.Break,
) (
entity.Expression,
error,
) {
if into != nil {
return nil, errors.Errorf (
brk.Position, "expected %v",
entity.FormatType(into))
}
loop, ok := this.topLoop()
if !ok {
return nil, errors.Errorf (
brk.Position,
"break statement must be within loop")
}
brk.Loop = loop
if loop.Type() != nil && brk.Value == nil {
return nil, errors.Errorf (
brk.Position,
"break statement must have value")
}
if brk.Value != nil {
value, err := this.analyzeExpression(loop.Type(), strict, brk.Value)
if err != nil { return nil, err }
brk.Value = value
}
return brk, nil
}
func (this *Tree) analyzeReturn (
into entity.Type,
mode strictness,
ret *entity.Return,
) (
entity.Expression,
error,
) {
if into != nil {
return nil, errors.Errorf (
ret.Position, "expected %v",
entity.FormatType(into))
}
ret.Declaration, _ = this.topDeclaration()
var ty entity.Type
switch ret.Declaration.(type) {
case *entity.Function:
ty = ret.Declaration.(*entity.Function).Signature.Return
case *entity.Method:
ty = ret.Declaration.(*entity.Method).Signature.Return
}
if ty != nil && ret.Value == nil {
return nil, errors.Errorf (
ret.Position,
"break statement must have value")
}
if ret.Value != nil {
value, err := this.analyzeExpression(ty, strict, ret.Value)
if err != nil { return nil, err }
ret.Value = value
}
return ret, nil
}

66
analyzer/function.go Normal file
View File

@ -0,0 +1,66 @@
package analyzer
import "git.tebibyte.media/fspl/fspl/errors"
import "git.tebibyte.media/fspl/fspl/entity"
func (this *Tree) analyzeFunction (
pos errors.Position,
key entity.Key,
) (
*entity.Function,
error,
) {
var err error
// return if exists already
if function, exists := this.Functions[key]; exists {
return function, nil
}
// error if function is missing
function, exists := this.rawFunctions[key]
if !exists {
return nil, errors.Errorf(pos, "no function named %s", key.Name)
}
// set unit
function.Unit = key.Unit
// functions cannot be marked as restricted
if function.Acc == entity.AccessRestricted {
return nil, errors.Errorf(pos, "cannot mark function as restricted")
}
// create a new scope context for this function
this.pushScopeContext(function)
this.pushScope(function)
defer this.popScopeContext()
defer this.popScope()
// analyze signature and add arguments to root scope of function
function.Signature, err = this.assembleSignatureMap(function.Signature)
if err != nil { return function, err }
for name, argument := range function.Signature.ArgumentMap {
argument.Ty, err = this.analyzeType(argument.Ty, false)
this.addVariable(argument)
function.Signature.ArgumentMap[name] = argument
if err != nil { return function, err }
}
function.Signature.Return, err =
this.analyzeType(function.Signature.Return, false)
if err != nil { return function, err }
// add incomplete function to complete functions because there is enough
// information for it to be complete from the point of view of other
// parts of the code
this.Functions[key] = function
// analyze function body
if function.Body != nil {
body, err := this.analyzeExpression (
function.Signature.Return, strict, function.Body)
if err != nil { return nil, err }
function.Body = body
}
return function, nil
}

35
analyzer/function_test.go Normal file
View File

@ -0,0 +1,35 @@
package analyzer
import "testing"
func TestFunctionUniqueErr (test *testing.T) {
testStringErr (test,
"hello already declared at stream0.fspl:2:1", 3, 1,
`
[hello] = { }
[hello] = { }
`)
}
func TestFunctionUnique (test *testing.T) {
testString (test,
`
[hello] = { }
[world] = { }
`)
}
func TestFunctionArgumentUniqueErr (test *testing.T) {
testStringErr (test,
"x already listed as argument at stream0.fspl:2:7", 2, 13,
`
[main x:Int x:U8 y:Int] = { }
`)
}
func TestFunctionArgumentUnique (test *testing.T) {
testString (test,
`
[main x:Int y:U8 z:Int] = { }
`)
}

297
analyzer/literal.go Normal file
View File

@ -0,0 +1,297 @@
package analyzer
import "unicode/utf16"
import "git.tebibyte.media/fspl/fspl/errors"
import "git.tebibyte.media/fspl/fspl/entity"
func (this *Tree) analyzeLiteralInt (
into entity.Type,
mode strictness,
literal *entity.LiteralInt,
) (
entity.Expression,
error,
) {
err := this.typeRestricted(literal.Position, into)
if err != nil { return nil, err }
if !isNumeric(into) {
return nil, errors.Errorf (
literal.Position, "cannot use integer literal as %v",
entity.FormatType(into))
}
if isInteger(into) && !inRange(into, int64(literal.Value)) {
return nil, errors.Errorf (
literal.Position, "integer literal out of range for type %v",
entity.FormatType(into))
}
literal.Ty = into
return literal, nil
}
func (this *Tree) analyzeLiteralFloat (
into entity.Type,
mode strictness,
literal *entity.LiteralFloat,
) (
entity.Expression,
error,
) {
err := this.typeRestricted(literal.Position, into)
if err != nil { return nil, err }
if !isFloat(into) {
return nil, errors.Errorf (
literal.Position, "cannot use float literal as %v",
entity.FormatType(into))
}
literal.Ty = into
return literal, nil
}
func (this *Tree) analyzeLiteralString (
into entity.Type,
mode strictness,
literal *entity.LiteralString,
) (
entity.Expression,
error,
) {
err := this.typeRestricted(literal.Position, into)
if err != nil { return nil, err }
base := ReduceToBase(into)
errCantUse := func () error {
return errors.Errorf (
literal.Position, "cannot use string literal as %v",
entity.FormatType(into))
}
fillUTF32 := func () {
literal.ValueUTF32 = []rune(literal.ValueUTF8)
}
fillUTF16 := func () {
literal.ValueUTF16 = utf16.Encode([]rune(literal.ValueUTF8))
}
switch base := base.(type) {
case *entity.TypeInt:
var length int; switch {
case base.Width >= 32:
fillUTF32()
length = len(literal.ValueUTF32)
case base.Width >= 16:
fillUTF16()
length = len(literal.ValueUTF16)
default:
length = len(literal.ValueUTF8)
}
if length > 1 {
return nil, errors.Errorf (
literal.Position,
"cannot fit string literal of length " +
"%v into %v",
length, entity.FormatType(into))
} else if length < 1 {
return nil, errors.Errorf (
literal.Position,
"string literal must have data when " +
"assigning to %v",
entity.FormatType(into))
}
case *entity.TypeArray:
element := ReduceToBase(base.Element)
if element, ok := element.(*entity.TypeInt); ok {
var length int; switch {
case element.Width >= 32:
fillUTF32()
length = len(literal.ValueUTF32)
case element.Width >= 16:
fillUTF16()
length = len(literal.ValueUTF16)
default:
length = len(literal.ValueUTF8)
}
if length > base.Length {
return nil, errors.Errorf (
literal.Position,
"cannot fit string literal of length " +
"%v into array of length %v",
length, base.Length)
}
} else {
return nil, errCantUse()
}
case *entity.TypeSlice:
element := ReduceToBase(base.Element)
if element, ok := element.(*entity.TypeInt); ok {
if element.Width >= 32 {
fillUTF32()
} else if element.Width >= 16 {
fillUTF16()
}
} else {
return nil, errCantUse()
}
case *entity.TypePointer:
referenced := ReduceToBase(base.Referenced)
if referenced, ok := referenced.(*entity.TypeInt); ok {
if referenced.Width != 8 {
return nil, errCantUse()
}
} else {
return nil, errCantUse()
}
default:
return nil, errCantUse()
}
literal.Ty = into
return literal, nil
}
func (this *Tree) analyzeLiteralArray (
into entity.Type,
mode strictness,
literal *entity.LiteralArray,
) (
entity.Expression,
error,
) {
err := this.typeRestricted(literal.Position, into)
if err != nil { return nil, err }
base := ReduceToBase(into)
var elementType entity.Type
switch base.(type) {
case *entity.TypeArray:
base := base.(*entity.TypeArray)
if base.Length < len(literal.Elements) {
return nil, errors.Errorf (
literal.Position, "expected %v elements or less",
base.Length)
}
elementType = base.Element
case *entity.TypeSlice:
base := base.(*entity.TypeSlice)
elementType = base.Element
case *entity.TypePointer:
base := base.(*entity.TypePointer)
elementType = base.Referenced
default:
return nil, errors.Errorf (
literal.Position, "cannot use array literal as %v",
entity.FormatType(into))
}
for index, element := range literal.Elements {
element, err := this.analyzeExpression(elementType, strict, element)
if err != nil { return nil, err }
literal.Elements[index] = element
}
literal.Ty = into
return literal, nil
}
func (this *Tree) analyzeLiteralStruct (
into entity.Type,
mode strictness,
literal *entity.LiteralStruct,
) (
entity.Expression,
error,
) {
err := this.typeRestricted(literal.Position, into)
if err != nil { return nil, err }
base, ok := ReduceToBase(into).(*entity.TypeStruct)
if !ok {
return nil, errors.Errorf (
literal.Position, "cannot use struct literal as %v",
entity.FormatType(into))
}
literal, err = this.assembleStructLiteralMap(literal)
if err != nil { return nil, err }
for name, member := range literal.MemberMap {
correct, ok := base.MemberMap[name]
if !ok {
return nil, errors.Errorf (
literal.Position, "no member %v.%s",
into, name)
}
value, err := this.analyzeExpression(correct.Type(), strict, member.Value)
if err != nil { return nil, err }
member.Value = value
}
literal.Ty = into
return literal, nil
}
func (this *Tree) analyzeLiteralBoolean (
into entity.Type,
mode strictness,
literal *entity.LiteralBoolean,
) (
entity.Expression,
error,
) {
err := this.typeRestricted(literal.Position, into)
if err != nil { return nil, err }
if !isBoolean(into) {
return nil, errors.Errorf (
literal.Position, "cannot use boolean literal as %v",
entity.FormatType(into))
}
literal.Ty = into
return literal, nil
}
func (this *Tree) analyzeLiteralNil (
into entity.Type,
mode strictness,
literal *entity.LiteralNil,
) (
entity.Expression,
error,
) {
err := this.typeRestricted(literal.Position, into)
if err != nil { return nil, err }
literal.Ty = into
return literal, nil
}
func (this *Tree) assembleStructLiteralMap (
literal *entity.LiteralStruct,
) (
*entity.LiteralStruct,
error,
) {
literal.MemberMap = make(map[string] *entity.Member)
literal.MemberOrder = make([]string, len(literal.Members))
for index, member := range literal.Members {
if previous, exists := literal.MemberMap[member.Name]; exists {
return literal, errors.Errorf (
member.Position, "%s already listed in struct literal at %v",
member.Name, previous.Position)
}
literal.MemberMap [member.Name] = member
literal.MemberOrder[index] = member.Name
literal.Members [index] = member
}
return literal, nil
}

53
analyzer/literal_test.go Normal file
View File

@ -0,0 +1,53 @@
package analyzer
import "testing"
func TestLiteralStructMemberUniqueErr (test *testing.T) {
testStringErr (test,
"x already listed in struct literal at stream0.fspl:5:3", 7, 3,
`
Point: (. x:Int y:Int z:Int)
[main] = {
point:Point = (.
x: 5
y: 0
x: 32)
}
`)
}
func TestLiteralStructMemberUnique (test *testing.T) {
testString (test,
`
Point: (. x:Int y:Int z:Int)
[main] = {
point:Point = (.
x: 5
y: 0
z: 32)
}
`)
}
func TestLiteralStructNested (test *testing.T) {
testString (test,
`
[main] = {
g:(. x:Int y:(. w:F64 z:F64)) = (.
x: 1
y: (.
w: 1.2
z: 78.5))
}
`)
}
func TestLiteralReferenceErr (test *testing.T) {
testStringErr (test,
"cannot assign to string literal", 3, 20,
`
[main] = {
byteptr:*Byte = [@'\n']
}
`)
}

178
analyzer/method.go Normal file
View File

@ -0,0 +1,178 @@
package analyzer
import "fmt"
import "git.tebibyte.media/fspl/fspl/errors"
import "git.tebibyte.media/fspl/fspl/entity"
// analyzeMethod analyzes a method that is directly owned by the specified type.
func (this *Tree) analyzeMethod (
pos errors.Position,
key entity.Key,
) (
*entity.Method,
error,
) {
// get parent typedef
typeKey := key.StripMethod()
owner, err := this.analyzeTypedef(pos, typeKey, false)
if err != nil { return nil, err }
// return if exists already
if method, exists := owner.Methods[key.Method]; exists {
return method, nil
}
// error if method is missing
method, exists := this.rawMethods[key]
if !exists {
return nil, errors.Errorf(pos, "no method named %v", key)
}
// set method's unit, very important information yes
method.Unit = owner.Unit
// methods cannot be marked as restricted
if method.Acc == entity.AccessRestricted {
return nil, errors.Errorf(pos, "cannot mark method as restricted")
}
// create a new scope context for this method
this.pushScopeContext(method)
this.pushScope(method)
defer this.popScopeContext()
defer this.popScope()
// analyze signature and add arguments to root scope of method
method.Signature, err = this.assembleSignatureMap(method.Signature)
if err != nil { return method, err }
for name, argument := range method.Signature.ArgumentMap {
argument.Ty, err = this.analyzeType(argument.Ty, false)
this.addVariable(argument)
method.Signature.ArgumentMap[name] = argument
if err != nil { return method, err }
}
method.Signature.Return, err =
this.analyzeType(method.Signature.Return, false)
if err != nil { return method, err }
// add owner to method
method.This = &entity.Declaration {
Position: method.Position,
Name: "this",
Ty: &entity.TypePointer {
Position: method.Position,
Referenced: &entity.TypeNamed {
Position: method.Position,
Unt: key.Unit,
Name: key.Name,
Type: owner.Type,
},
},
}
this.addVariable(method.This)
// add incomplete method to complete methods because there is enough
// information for it to be complete from the point of view of other
// parts of the code
owner.Methods[key.Method] = method
// analyze method body
if method.Body != nil {
body, err := this.analyzeExpression (
method.Signature.Return, strict, method.Body)
if err != nil { return nil, err }
method.Body = body
}
return method, nil
}
func (this *Tree) methodExists (pos errors.Position, key entity.Key) (bool, error) {
// check raw map
_, existsInRaw := this.rawMethods[key]
if existsInRaw { return true, nil }
// check parent typedef
typeKey := key.StripMethod()
owner, err := this.analyzeTypedef(pos, typeKey, false)
if err != nil { return false, err }
_, existsInType := owner.Methods[key.Method]
return existsInType, nil
}
// analyzeMethodOrBehavior returns *entity.Signature if it found an interface
// behavior, and *entity.Method if it found a method.
func (this *Tree) analyzeMethodOrBehavior (
pos errors.Position,
ty entity.Type,
name string,
) (
any,
error,
) {
return this.analyzeMethodOrBehaviorInternal(pos, ty, name, true)
}
func (this *Tree) analyzeMethodOrBehaviorInternal (
pos errors.Position,
ty entity.Type,
name string,
pierceReference bool,
) (
any,
error,
) {
switch ty.(type) {
case *entity.TypeNamed:
ty := ty.(*entity.TypeNamed)
key := entity.Key {
Unit: ty.Type.Unit(),
Name: ty.Name,
Method: name,
}
exists, err := this.methodExists(pos, key)
if err != nil { return nil, err }
if exists {
method, err := this.analyzeMethod(pos, key)
if err != nil { return nil, err }
return method, nil
} else {
return this.analyzeMethodOrBehaviorInternal (
pos, ty.Type, name, false)
}
case *entity.TypeInterface:
ty := ty.(*entity.TypeInterface)
if behavior, ok := ty.BehaviorMap[name]; ok {
return behavior, nil
} else {
return nil, errors.Errorf (
pos, "no behavior or method named %s",
name)
}
case *entity.TypePointer:
if pierceReference {
ty := ty.(*entity.TypePointer)
return this.analyzeMethodOrBehaviorInternal (
pos, ty.Referenced, name, false)
} else {
return nil, errors.Errorf (
pos, "no method named %s defined on this type",
name)
}
case *entity.TypeSlice,
*entity.TypeArray,
*entity.TypeStruct,
*entity.TypeInt,
*entity.TypeFloat,
*entity.TypeWord:
return nil, errors.Errorf (
pos, "no method named %s defined on this type",
name)
default: panic(fmt.Sprint("BUG: analyzer doesnt know about type ", ty))
}
}

69
analyzer/method_test.go Normal file
View File

@ -0,0 +1,69 @@
package analyzer
import "testing"
func TestMethodUniqueErr (test *testing.T) {
testStringErr (test,
"Bird.fly already declared at stream0.fspl:3:1", 4, 1,
`
Bird: Int
Bird.[fly] = { }
Bird.[fly distance:Int] = { }
`)
}
func TestMethodUnique (test *testing.T) {
testString (test,
`
Bird: Int
Bird.[fly] = { }
Bird.[land] = { }
Bird.[walk distance:Int] = { }
Bat: Int
Bat.[fly] = { }
[fly] = { }
`)
}
func TestMethodArgumentUniqueErr (test *testing.T) {
testStringErr (test,
"x already listed as argument at stream0.fspl:3:12", 3, 18,
`
Bird: Int
Bird.[main x:Int x:U8 y:Int] = { }
`)
}
func TestMethodArgumentUnique (test *testing.T) {
testString (test,
`
Bird: Int
Bird.[main x:Int y:U8 z:Int] = { }
`)
}
func TestMethodThis (test *testing.T) {
testString (test,
`
Number: Int
Number.[add x:Number]:Number = [++[.this] x]
StringHolder: (. string:String)
StringHolder.[setString string:String] = {
this.string = string
}
`)
}
func TestMethodChained (test *testing.T) {
testString (test,
`
Number: Int
Number.[add x:Number]:Number = [+ [.this] x]
Number.[sub x:Number]:Number = [- [.this] x]
Number.[mul x:Number]:Number = [* [.this] x]
Number.[div x:Number]:Number = [/ [.this] x]
[main]: Number = [~Number 5].[add 8].[mul 3]
`)
}

20
analyzer/misc.go Normal file
View File

@ -0,0 +1,20 @@
package analyzer
import "git.tebibyte.media/fspl/fspl/errors"
import "git.tebibyte.media/fspl/fspl/entity"
func (this *Tree) assembleSignatureMap (signature *entity.Signature) (*entity.Signature, error) {
signature.ArgumentMap = make(map[string] *entity.Declaration)
signature.ArgumentOrder = make([]string, len(signature.Arguments))
for index, member := range signature.Arguments {
if previous, exists := signature.ArgumentMap[member.Name]; exists {
return signature, errors.Errorf (
member.Position, "%s already listed as argument at %v",
member.Name, previous.Position)
}
signature.ArgumentMap [member.Name] = member
signature.ArgumentOrder[index] = member.Name
signature.Arguments [index] = member
}
return signature, nil
}

33
analyzer/modules-plan.md Normal file
View File

@ -0,0 +1,33 @@
note to self clean this up afterward and move it into design/modules because
this realllllly sucks.
semantic tree: index top-level declarations by a key:
Key struct {
UUID
Name
}
Tree.Analyze should take in:
UUID
Syntax tree
only analyze one unit at a time.
parser should have no idea about what a unit is, and UUID fields in top level declarations are to be considered semantic.
important: in order for the compiler to make sure everything is analyzed, it does this
module:
take in semantic tree, address, skimming book as parameters
parse metadata file
if currently parsing a unit with this uuid, error
push this units uuid onto a stack
for each dependency unit, recurse, passing in semantic tree and true to skimming
parse unit files into one syntax tree. if skimming, do a skim parse
analyze syntax tree into semantic tree
pop this units uuid off of the stack
have a simpler version for source files
this routine analyzes leaf units (no dependencies) first, which makes them available in the semantic tree for their dependents to depend on. this travels upward the root is reached.

219
analyzer/multiunit_test.go Normal file
View File

@ -0,0 +1,219 @@
package analyzer
import "testing"
func TestTwoUnit (test *testing.T) {
testUnits (test,
`[main]:something::X = 5`,
"something.fspl",
`+ X:Int`,
)}
func TestUnitPrivateTypeErr (test *testing.T) {
testUnitsErr (test,
"main.fspl", "type other::X is private", 1, 8,
`[main]:other::X = 5`,
"other.fspl",
`- X:Int`,
)}
func TestUnitPrivateFunctionErr (test *testing.T) {
testUnitsErr (test,
"main.fspl", "function other::[x] is private", 1, 11,
`[y]:Int = other::[x]`,
"other.fspl",
`- [x]:Int = 5`,
)}
func TestUnitPrivateMethodErr (test *testing.T) {
testUnitsErr (test,
"main.fspl", "method T.[x] is private", 1, 21,
`[y]:Int = z:other::T.[x]`,
"other.fspl",
`
+ T:Int
- T.[x]:Int = 5`,
)}
func TestUnitAssignRestricted (test *testing.T) {
testUnits (test,
`[main] = {
x:other::RestrictedInt
y:other::RestrictedInt
x = y
}`,
"other.fspl",
`~ RestrictedInt:Int`,
)}
func TestUnitAssignLiteralRestrictedErr (test *testing.T) {
testUnitsErr (test,
"main.fspl", "type other::RestrictedInt is restricted", 1, 31,
`[main]:other::RestrictedInt = 5`,
"other.fspl",
`~ RestrictedInt:Int`,
)}
func TestAssignLiteralRestricted (test *testing.T) {
testString (test,
`~ RestrictedInt:Int
[main]:RestrictedInt = 5`,
)}
func TestUnitMemberAccessRestrictedErr (test *testing.T) {
testUnitsErr (test,
"main.fspl", "type other::RestrictedStruct is restricted", 3, 3,
`[main] = {
x:other::RestrictedStruct
x.x = 5
}`,
"other.fspl",
`~ RestrictedStruct:(. x:Int y:Int)`,
)}
func TestMemberAccessRestricted (test *testing.T) {
testString (test,
`~ RestrictedStruct:(. x:Int y:Int)
[main] = {
x:RestrictedStruct
x.x = 5
}`,
)}
func TestUnitSubscriptRestrictedErr (test *testing.T) {
testUnitsErr (test,
"main.fspl", "type other::RestrictedArr is restricted", 3, 4,
`[main] = {
x:other::RestrictedArr
[.x 0] = 5
}`,
"other.fspl",
`~ RestrictedArr:5:Int`,
)}
func TestSubscriptRestricted (test *testing.T) {
testString (test,
`~ RestrictedArr:5:Int
[main] = {
x:RestrictedArr
[.x 0] = 5
}`,
)}
func TestUnitMathRestrictedErr (test *testing.T) {
testUnitsErr (test,
"main.fspl", "type other::RestrictedInt is restricted", 2, 27,
`[main] = {
x:other::RestrictedInt = [+
y:other::RestrictedInt
z:other::RestrictedInt]
}`,
"other.fspl",
`~ RestrictedInt:Int`,
)}
func TestMathRestricted (test *testing.T) {
testString (test,
`~ RestrictedInt:Int
[main] = {
x:RestrictedInt = [+
y:RestrictedInt
z:RestrictedInt]
}`,
)}
func TestNestedUnitTypedef (test *testing.T) {
testUnits (test,
`[main]:layer1::X = 5`,
"layer0.fspl",
`+ X:Int`,
"layer1.fspl",
`+ X:layer0::X`,
)}
func TestUnitBehaviorCallRestrictedErr (test *testing.T) {
testUnitsErr (test,
"main.fspl", "type other::RestrictedInterface is restricted", 3, 3,
`[main] = {
x:other::RestrictedInterface
x.[y]
}`,
"other.fspl",
`~ RestrictedInterface:(~ [y])`,
)}
func TestBehaviorCallRestricted (test *testing.T) {
testString (test,
`[main] = {
x:RestrictedInterface
x.[y]
}
~ RestrictedInterface:(~ [y])`,
)}
func TestUnitCastRestrictedErr (test *testing.T) {
testUnitsErr (test,
"main.fspl", "type other::RestrictedInt is restricted", 2, 16,
`[main] = {
x:Int = [~Int y:other::RestrictedInt]
}`,
"other.fspl",
`~ RestrictedInt:Int`,
)}
func TestCastRestricted (test *testing.T) {
testString (test,
`[main] = {
x:Int = [~Int y:RestrictedInt]
}
~ RestrictedInt:Int`,
)}
func TestFunctionRestrictedErr (test *testing.T) {
testStringErr (test,
"cannot mark function as restricted", 1, 1,
`~ [f]`,
)}
func TestMethodRestrictedErr (test *testing.T) {
testStringErr (test,
"cannot mark method as restricted", 2, 1,
`T:Int
~ T.[f]`,
)}
func TestUnitInterfaceSatisfaction (test *testing.T) {
testUnits (test,
`[sayHello writer:io::Writer] = writer.[write 'well hello their\n']
[main]: I32 'main' = {
stdout:io::File = 1
[sayHello stdout]
0
}`,
"cstdio.fspl",
`+ FileDescriptor: Int
+ [write file:FileDescriptor buffer:*Byte count:Index]: Index 'write'`,
"io.fspl",
`+ Writer: (~ [write buffer:*:Byte]: Index)
+ File: cstdio::FileDescriptor
+ File.[write buffer:*:Byte]:Index =
cstdio::[write
[~cstdio::FileDescriptor [.this]]
[~*Byte buffer] [#buffer]]`,
)}

View File

@ -2,141 +2,29 @@ package analyzer
import "testing"
func TestFunctionUniqueErr (test *testing.T) {
testStringErr (test,
"hello already declared at stream0.fspl:2:1", 3, 1,
`
[hello] = { }
[hello] = { }
`)
}
func TestFunctionUniqueErrShadowBuiltin (test *testing.T) {
testStringErr (test,
"cannot shadow builtin true ", 2, 2,
`
[true]:Bool = false
`)
}
func TestFunctionUnique (test *testing.T) {
testString (test,
`
[hello] = { }
[world] = { }
`)
}
func TestTypeUniqueErr (test *testing.T) {
testStringErr (test,
"hello already declared at stream0.fspl:2:1", 3, 1,
`
hello: *Int
hello: (x:Int y:Int)
`)
}
func TestTypeUniqueErrShadowBuiltin (test *testing.T) {
testStringErr (test,
"cannot shadow builtin true ", 2, 2,
`
true:Int
`)
}
func TestTypeUnique (test *testing.T) {
testString (test,
`
hello: *Int
world: (x:Int y:Int)
`)
}
func TestTypeNameErrMissing (test *testing.T) {
testStringErr (test,
"no type named example", 2, 20,
`
[main] = { example:Example }
`)
}
func TestTypeName (test *testing.T) {
testString (test,
`
Example: Int
AllBuiltin: (
int: Int
uint: UInt
byte: Byte
rune: Rune
string: String
i8: I8
i8: I16
i8: I32
i8: I64
u8: U8
u8: U16
u8: U32
u8: U64
f32: F32
f64: F64
bool: Bool)
[main] = {
example:Example
allBuiltin:AllBuiltin
}
`)
}
func TestFunctionUniqueErr (test *testing.T) {
testStringErr (test,
"hello already declared at stream0.fspl:2:1", 3, 1,
`
hello: *Int
[hello] = { }
`)
}
func TestFunctionUnique (test *testing.T) {
testString (test,
`
hello: *Int
[world] = { }
`)
}
func TestMethodUniqueErr (test *testing.T) {
testStringErr (test,
"Bird.fly already declared at stream0.fspl:3:1", 4, 1,
`
Bird: Int
Bird::[fly] = { }
Bird::[fly distance:Int] = { }
`)
}
func TestMethodUnique (test *testing.T) {
testString (test,
`
Bird: Int
Bird::[fly] = { }
Bird::[land] = { }
Bird::[walk distance:Int] = { }
Bat: Int
Bat::[fly] = { }
[fly] = { }
`)
}
func TestMethodNameErrMissing (test *testing.T) {
testStringErr (test,
"no method Type::world", 5, 3,
"no method named world defined on this type", 6, 10,
`
Type: Int
Type::[something] = { }
Type.[something] = { }
[main] = {
instance:Type
[instance::world]
instance.[world]
}
`)
}
func TestMethodNameErrBadLayer (test *testing.T) {
testStringErr (test,
"no method named something defined on this type", 7, 10,
`
Type: Int
Type.[something] = { }
RefType: *Type
[main] = {
instance:RefType
instance.[something]
}
`)
}
@ -145,90 +33,16 @@ func TestMethodName (test *testing.T) {
testString (test,
`
Type: Int
Type::[world] = { }
Type.[world] = { }
[main] = {
instance:Type
[instance::world]
instance.[world]
ref:*Type = [@ instance]
ref.[world]
}
`)
}
func TestBehaviorUniqueErr (test *testing.T) {
testStringErr (test,
"fly already listed in interface at stream0.fspl:2:8", 2, 14,
`
Bird: ([fly] [fly])
`)
}
func TestBehaviorUnique (test *testing.T) {
testString (test,
`
Bird: ([fly] [land])
`)
}
func TestBehaviorNameErrMissing (test *testing.T) {
testStringErr (test,
"no behavior Bird::swim", 8, 3,
`
Bird: ([fly] [land])
BirdImpl: Int
BirdImpl::[fly] = { }
BirdImpl::[land] = { }
[main] = {
bird:Bird = [@impl:BirdImpl]
[bird::swim]
}
`)
}
func TestBehaviorName (test *testing.T) {
testString (test,
`
Bird: ([fly] [land])
BirdImpl: Int
BirdImpl::[fly] = { }
BirdImpl::[land] = { }
[main] = {
bird:Bird = [@impl:BirdImpl]
[bird::fly]
}
`)
}
func TestFunctionArgumentUniqueErr (test *testing.T) {
testStringErr (test,
"x already declared in block at stream0.fspl:2:8", 2, 14,
`
[main x:Int x:U8 y:Int] = { }
`)
}
func TestFunctionArgumentUnique (test *testing.T) {
testString (test,
`
[main x:Int y:U8 z:Int] = { }
`)
}
func TestMethodArgumentUniqueErr (test *testing.T) {
testStringErr (test,
"x already declared in block at stream0.fspl:3:13", 3, 19,
`
Bird: Int
Bird::[main x:Int x:U8 y:Int] = { }
`)
}
func TestMethodArgumentUnique (test *testing.T) {
testString (test,
`
Bird: Int
Bird::[main x:Int y:U8 z:Int] = { }
`)
}
func TestVariableUniqueErr (test *testing.T) {
testStringErr (test,
"x already declared in block at stream0.fspl:3:2", 5, 2,
@ -241,20 +55,10 @@ testStringErr (test,
`)
}
func TestVariableUniqueErrShadowBuiltin (test *testing.T) {
testStringErr (test,
"cannot shadow builtin false ", 3, 2,
`
[main] = {
false:Bool = true
}
`)
}
func TestVariableUnique (test *testing.T) {
testString (test,
`
x: Int
[x] = { }
[y] = { }
[main x:Int] = {
x:Int
@ -268,20 +72,9 @@ x: Int
`)
}
func TestVariableNameErrType (test *testing.T) {
testStringErr (test,
"named type example cannot be used as location expression", 4, 2,
`
example: Int
[main] = {
example = 5
}
`)
}
func TestVariableNameErrFunction (test *testing.T) {
testStringErr (test,
"function example cannot be used as location expression", 4, 2,
"no variable named example", 4, 2,
`
[example] = { }
[main] = {
@ -292,7 +85,7 @@ testStringErr (test,
func TestVariableNameErrMissing (test *testing.T) {
testStringErr (test,
"no variable named example", 4, 2,
"no variable named example", 3, 2,
`
[main] = {
example = 5
@ -328,96 +121,13 @@ testString (test,
example = 3
}
}
}
`)
}
func TestStructMemberUniqueErr (test *testing.T) {
testStringErr (test,
"x already listed in struct at stream0.fspl:3:2", 6, 2,
`
Bird: (
x:Int
y:Int
z:Int
x:U8)
`)
}
func TestStructMemberUnique (test *testing.T) {
testString (test,
`
Bird: (
x:Int
y:Int
z:Int
a:U8)
`)
}
func TestStructMemberNameErrMissing (test *testing.T) {
testStringErr (test,
"no member Type.world", 5, 2,
`
Type: (something:Int)
[main] = {
instance:Type
instance.world = 5
}
`)
}
func TestStructMemberName (test *testing.T) {
testString (test,
`
Type: (world:Int)
[main] = {
instance:Type
instance.world = 5
}
`)
}
func TestStructLiteralMemberUniqueErr (test *testing.T) {
testStringErr (test,
"x already listed in struct literal at stream0.fspl:5:2", 7, 2,
`
Point: (x:Int y:Int z:Int)
[main] = {
point:Point = (
x: 5
y: 0
x: 32)
}
`)
}
func TestStructLiteralMemberUnique (test *testing.T) {
testString (test,
`
Point: (x:Int y:Int z:Int)
[main] = {
point:Point = (
x: 5
y: 0
z: 32)
}
`)
}
func TestFunctionNameErrType (test *testing.T) {
testStringErr (test,
"cannot call named type example", 3, 3,
`
example: Int
[main] = [example]
`)
}
func TestFunctionNameErrVar (test *testing.T) {
testStringErr (test,
"cannot call example:Int", 4, 3,
"no function named example", 4, 2,
`
[main] = {
example:Int
@ -428,7 +138,7 @@ testStringErr (test,
func TestFunctionNameErrMissing (test *testing.T) {
testStringErr (test,
"no function named example", 2, 3,
"no function named example", 2, 10,
`
[main] = [example]
`)
@ -441,3 +151,55 @@ testString (test,
[main] = [example]
`)
}
func TestInterfaceBehaviorNameErrMissing (test *testing.T) {
testStringErr (test,
"no behavior or method named swim", 8, 6,
`
Bird: (~ [fly] [land])
BirdImpl: Int
BirdImpl.[fly] = { }
BirdImpl.[land] = { }
[main] = {
bird:Bird = impl:BirdImpl
bird.[swim]
}
`)
}
func TestInterfaceBehaviorName (test *testing.T) {
testString (test,
`
Bird: (~ [fly] [land])
BirdImpl: Int
BirdImpl.[fly] = { }
BirdImpl.[land] = { }
[main] = {
bird:Bird = impl:BirdImpl
bird.[fly]
}
`)
}
func TestStructMemberNameErrMissing (test *testing.T) {
testStringErr (test,
"no member instance.world", 5, 10,
`
Type: (. something:Int)
[main] = {
instance:Type
instance.world = 5
}
`)
}
func TestStructMemberName (test *testing.T) {
testString (test,
`
Type: (. world:Int)
[main] = {
instance:Type
instance.world = 5
}
`)
}

View File

@ -2,112 +2,82 @@ package analyzer
import "testing"
func TestOperatorModUnderArgCountErr (test *testing.T) {
func TestOperationModUnderArgCountErr (test *testing.T) {
testStringErr (test,
"wrong argument count for %", 10, 2,
"wrong argument count for %", 2, 10,
`
[main] = [% 2]
`)
}
func TestOperatorModOverArgCountErr (test *testing.T) {
func TestOperationModOverArgCountErr (test *testing.T) {
testStringErr (test,
"wrong argument count for %", 10, 2,
"wrong argument count for %", 2, 10,
`
[main] = [% 2 3 1]
`)
}
func TestOperatorLogicalNegationUnderArgCountErr (test *testing.T) {
func TestOperationLogicalNegationOverArgCountErr (test *testing.T) {
testStringErr (test,
"wrong argument count for !", 10, 2,
`
[main] = [!]
`)
}
func TestOperatorLogicalNegationOverArgCountErr (test *testing.T) {
testStringErr (test,
"wrong argument count for !", 10, 2,
"wrong argument count for !", 2, 10,
`
[main] = [! 348 92]
`)
}
func TestOperatorBitwiseNegationUnderArgCountErr (test *testing.T) {
func TestOperationBitwiseNegationOverArgCountErr (test *testing.T) {
testStringErr (test,
"wrong argument count for !!", 10, 2,
`
[main] = [!!]
`)
}
func TestOperatorBitwiseNegationOverArgCountErr (test *testing.T) {
testStringErr (test,
"wrong argument count for !!", 10, 2,
"wrong argument count for !!", 2, 10,
`
[main] = [!! 348 92]
`)
}
func TestOperatorBitShiftLeftUnderArgCountErr (test *testing.T) {
func TestOperationBitShiftLeftOverArgCountErr (test *testing.T) {
testStringErr (test,
"wrong argument count for <<", 10, 2,
`
[main] = [<<]
`)
}
func TestOperatorBitShiftLeftOverArgCountErr (test *testing.T) {
testStringErr (test,
"wrong argument count for <<", 10, 2,
"wrong argument count for <<", 2, 10,
`
[main] = [<< 348 92 324]
`)
}
func TestOperatorBitShiftRightUnderArgCountErr (test *testing.T) {
func TestOperationBitShiftRightOverArgCountErr (test *testing.T) {
testStringErr (test,
"wrong argument count for >>", 10, 2,
`
[main] = [>>]
`)
}
func TestOperatorBitShiftRightOverArgCountErr (test *testing.T) {
testStringErr (test,
"wrong argument count for >>", 10, 2,
"wrong argument count for >>", 2, 10,
`
[main] = [>> 348 92 324]
`)
}
func TestOperatorArgCount (test *testing.T) {
func TestOperationArgCount (test *testing.T) {
testString (test,
`
[main] = {
[+ 1 2 3 4]
[++ 1]
[- 1 2 3 4]
[-- 1]
[* 5 6]
[/ 32 16]
[% 5 6]
[!! true]
[|| false]
[&& true true false true]
[^^ true true false true]
[! 1]
[| 1 2]
[& 3 1]
[^ 1 2 3 4]
[<< 1 8]
[>> 8 1]
[< 1 2 3 4]
[> 1 2 3 4]
[<= 10 5 3]
[>= 1 2 3 3 4]
[= 2 2 2 3 9]
x:Int
b:Bool
x = [+ 1 2 3 4]
x = [++ 1]
x = [- 1 2 3 4]
x = [-- 1]
x = [* 5 6]
x = [/ 32 16]
x = [% 5 6]
x = [!! 1]
x = [|| 1 2]
x = [&& 3 1 5]
x = [^^ 1 2 3 4]
b = [! true]
b = [| true false]
b = [& true true]
b = [^ true true false true]
x = [<< 1 8]
x = [>> 8 1]
b = [< x 2 3 4]
b = [> x 2 3 4]
b = [<= x 5 3]
b = [>= x 2 3 3 4]
b = [= x 2 2 3 9]
}
`)
}

132
analyzer/scope.go Normal file
View File

@ -0,0 +1,132 @@
package analyzer
import "git.tebibyte.media/fspl/fspl/entity"
// scopeContextManager is a stack of scopeContexts allowing multiple stacks of
// scopes to be managed at the same time in case the analysis of one scoped
// entity causes the analysis of another.
type scopeContextManager []scopeContext
func (this *scopeContextManager) pushScopeContext (declaration entity.TopLevel) {
*this = append(*this, scopeContext { declaration: declaration })
}
func (this *scopeContextManager) popScopeContext () {
this.assertPopulated()
*this = (*this)[:len(*this) - 1]
}
func (this *scopeContextManager) pushLoop (loop *entity.Loop) {
this.assertPopulated()
(*this)[len(*this) - 1].pushLoop(loop)
}
func (this *scopeContextManager) popLoop () {
this.assertPopulated()
(*this)[len(*this) - 1].popLoop()
}
func (this *scopeContextManager) topLoop () (*entity.Loop, bool) {
this.assertPopulated()
return (*this)[len(*this) - 1].topLoop()
}
func (this *scopeContextManager) topDeclaration () (entity.TopLevel, bool) {
if len(*this) < 1 { return nil, false }
return (*this)[len(*this) - 1].declaration, true
}
func (this *scopeContextManager) pushScope (scope entity.Scoped) {
this.assertPopulated()
(*this)[len(*this) - 1].pushScope(scope)
}
func (this *scopeContextManager) popScope () {
this.assertPopulated()
(*this)[len(*this) - 1].popScope()
}
func (this *scopeContextManager) topScope () (entity.Scoped, bool) {
this.assertPopulated()
return (*this)[len(*this) - 1].topScope()
}
func (this *scopeContextManager) variable (name string) *entity.Declaration {
this.assertPopulated()
return (*this)[len(*this) - 1].variable(name)
}
func (this *scopeContextManager) addVariable (declaration *entity.Declaration) {
this.assertPopulated()
(*this)[len(*this) - 1].addVariable(declaration)
}
func (this *scopeContextManager) assertPopulated () {
if len(*this) < 1 {
panic("scopeContextManager must have at least 1 scope context")
}
}
// scopeContext is a stack of scopes and loops used when analyzing scoped
// entities.
type scopeContext struct {
scopes []entity.Scoped
loops []*entity.Loop
declaration entity.TopLevel
}
func (this *scopeContext) pushLoop (loop *entity.Loop) {
this.loops = append(this.loops, loop)
}
func (this *scopeContext) popLoop () {
this.assertLoopPopulated()
this.loops = this.loops[:len(this.loops) - 1]
}
func (this *scopeContext) topLoop () (*entity.Loop, bool) {
if len(this.loops) < 1 { return nil, false }
return this.loops[len(this.loops) - 1], true
}
func (this *scopeContext) pushScope (scope entity.Scoped) {
this.scopes = append(this.scopes, scope)
}
func (this *scopeContext) popScope () {
this.assertScopePopulated()
this.scopes = this.scopes[:len(this.scopes) - 1]
}
func (this *scopeContext) topScope () (entity.Scoped, bool) {
if len(this.scopes) < 1 { return nil, false }
return this.scopes[len(this.scopes) - 1], true
}
func (this *scopeContext) variable (name string) *entity.Declaration {
this.assertScopePopulated()
for index := len(this.scopes) - 1; index >= 0; index -- {
variable := this.scopes[index].Variable(name)
if variable != nil {
return variable
}
}
return nil
}
func (this *scopeContext) addVariable (declaration *entity.Declaration) {
this.assertScopePopulated()
this.scopes[len(this.scopes) - 1].AddVariable(declaration)
}
func (this *scopeContext) assertScopePopulated () {
if len(this.scopes) < 1 {
panic("scopeContext must have at least 1 scope")
}
}
func (this *scopeContext) assertLoopPopulated () {
if len(this.scopes) < 1 {
panic("scopeContext must have at least 1 loop")
}
}

View File

@ -1,82 +1,199 @@
package analyzer
import "io"
import "fmt"
import "testing"
import "strings"
import "github.com/alecthomas/participle/v2"
import "git.tebibyte.media/sashakoshka/fspl/parser"
import "github.com/google/uuid"
import "git.tebibyte.media/fspl/fspl/lexer"
import "git.tebibyte.media/fspl/fspl/errors"
import "git.tebibyte.media/fspl/fspl/entity"
import "git.tebibyte.media/fspl/fspl/parser/fspl"
func testStringErr (
test *testing.T,
errMessage string,
errLine int,
errColumn int,
errRow int,
errStart int,
input string,
) {
testReaderErr (
test,
errMessage, errLine, errColumn,
strings.NewReader(input))
}
func testReaderErr (
test *testing.T,
errMessage string,
errLine int,
errColumn int,
inputs ...io.Reader,
) {
ast := parser.Tree { }
for index, stream := range inputs {
err := ast.Parse(fmt.Sprintf("stream%d.fspl", index), stream)
if err != nil {
test.Error("parser returned error: ", err)
return
}
}
address := entity.Address("stream0.fspl")
ast, ok := treeOf(test, address, input, false)
if !ok { return }
tree := Tree { }
err := tree.Analyze(ast)
err := tree.Analyze(address.UUID(), nil, ast)
if err == nil {
test.Error("analyzer did not return error")
return
}
got := err.(participle.Error)
gotMessage := got.Message()
gotLine := got.Position().Line
gotColumn := got.Position().Column
correct :=
gotMessage == errMessage &&
gotLine == errLine &&
gotColumn == errColumn
if !correct {
test.Log("errors do not match")
test.Logf("got:\n%v:%v: %v", gotLine, gotColumn, gotMessage)
test.Logf("correct:\n%v:%v: %v", errLine, errColumn, errMessage)
test.Fail()
return
}
compareErr(test, address, string(address), errMessage, errRow, errStart, err)
}
func testString (test *testing.T, input string) {
testReader(test, strings.NewReader(input))
}
func testReader (test *testing.T, inputs ...io.Reader) {
ast := parser.Tree { }
for index, stream := range inputs {
err := ast.Parse(fmt.Sprintf("stream%d.fspl", index), stream)
if err != nil {
test.Error("parser returned error: ", err)
return
}
}
address := entity.Address("stream0.fspl")
ast, ok := treeOf(test, address, input, false)
if !ok { return }
tree := Tree { }
err := tree.Analyze(ast)
err := tree.Analyze(address.UUID(), nil, ast)
if err != nil {
test.Error("analyzer returned error: ", err)
test.Error("analyzer returned error:\n" + errors.Format(err))
return
}
}
func testUnits (
test *testing.T,
main string,
dependencies ...string,
) {
if len(dependencies) % 2 != 0 {
panic("dependencies len must be even. forget an address?")
}
tree := Tree { }
nicknames := make(map[string] uuid.UUID)
// dependencies
for index := 0; index < len(dependencies) - 1; index += 2 {
address := entity.Address(dependencies[index])
source := dependencies[index + 1]
ast, ok := treeOf(test, address, source, true)
if !ok { return }
err := tree.Analyze(address.UUID(), nicknames, ast)
if err != nil {
test.Error("analyzer returned error:\n" + errors.Format(err))
return
}
unit := address.UUID()
nickname, ok := address.Nickname()
if !ok {
test.Errorf("could not generate nickname for %v", address)
return
}
nicknames[nickname] = unit
}
// main
address := entity.Address("main.fspl")
ast, ok := treeOf(test, address, main, false)
if !ok { return }
err := tree.Analyze(address.UUID(), nicknames, ast)
if err != nil {
test.Error("analyzer returned error:\n" + errors.Format(err))
return
}
}
func testUnitsErr (
test *testing.T,
errFile string,
errMessage string,
errRow int,
errStart int,
main string,
dependencies ...string,
) {
if len(dependencies) % 2 != 0 {
panic("dependencies len must be even. forget an address?")
}
tree := Tree { }
nicknames := make(map[string] uuid.UUID)
// dependencies
for index := 0; index < len(dependencies) - 1; index += 2 {
address := entity.Address(dependencies[index])
source := dependencies[index + 1]
ast, ok := treeOf(test, address, source, true)
if !ok { return }
err := tree.Analyze(address.UUID(), nicknames, ast)
if err != nil {
compareErr(test, address, errFile, errMessage, errRow, errStart, err)
return
}
unit := address.UUID()
nickname, ok := address.Nickname()
if !ok {
test.Errorf("could not generate nickname for %v", address)
return
}
nicknames[nickname] = unit
}
// main
address := entity.Address("main.fspl")
ast, ok := treeOf(test, address, main, false)
if !ok { return }
err := tree.Analyze(address.UUID(), nicknames, ast)
if err != nil {
compareErr(test, address, errFile, errMessage, errRow, errStart, err)
return
}
test.Error("analyzer did not return error")
}
func treeOf (test *testing.T, address entity.Address, input string, skim bool) (fsplParser.Tree, bool) {
ast := fsplParser.Tree { }
lx, err := lexer.LexReader (
string(address),
strings.NewReader(input))
if err != nil {
test.Error("lexer returned error:\n" + errors.Format(err))
return ast, false
}
if skim {
err = ast.Skim(lx)
} else {
err = ast.Parse(lx)
}
if err != nil {
test.Error("parser returned error:\n" + errors.Format(err))
return ast, false
}
return ast, true
}
func compareErr (
test *testing.T,
address entity.Address,
errFile string,
errMessage string,
errRow int,
errStart int,
err error,
) bool {
got := err.(errors.Error)
gotMessage := got.Error()
gotFile := got.Position().File
gotRow := got.Position().Row + 1
gotStart := got.Position().Start + 1
correct :=
gotFile == errFile &&
gotMessage == errMessage &&
gotRow == errRow &&
gotStart == errStart
if correct { return true }
test.Log("errors do not match:")
if gotMessage != errMessage {
test.Log("- messages do not match:")
test.Logf(" [%s]", gotMessage)
test.Logf(" [%s]", errMessage)
}
if gotRow != errRow {
test.Logf("- rows do not match: (%d, %d)", gotRow, errRow)
}
if gotStart != errStart {
test.Logf("- columns do not match: (%d, %d)", gotStart, errStart)
}
test.Log("got:\n" + errors.Format(got))
test.Logf("correct:\n%v:%v: %v", errRow, errStart, errMessage)
test.Fail()
return false
}

View File

@ -1,27 +1,39 @@
package analyzer
import "github.com/alecthomas/participle/v2"
import "github.com/alecthomas/participle/v2/lexer"
import "git.tebibyte.media/sashakoshka/fspl/entity"
import "git.tebibyte.media/sashakoshka/fspl/parser"
import "github.com/google/uuid"
import "git.tebibyte.media/fspl/fspl/errors"
import "git.tebibyte.media/fspl/fspl/entity"
import "git.tebibyte.media/fspl/fspl/parser/fspl"
// Tree is a semantic tree. It contains the same constructs as the syntax tree,
// but with their semantic information filled in.
type Tree struct {
rawTypes map[string] *entity.Typedef
rawFunctions map[string] *entity.Function
rawMethods map[string] *entity.Method
ast parser.Tree
Types map[entity.Key] *entity.Typedef
Functions map[entity.Key] *entity.Function
Types map[string] entity.Type
Functions map[string] *entity.Function
unit uuid.UUID
ast fsplParser.Tree
nicknames map[string] uuid.UUID
rawTypes map[entity.Key] *entity.Typedef
rawFunctions map[entity.Key] *entity.Function
rawMethods map[entity.Key] *entity.Method
scopeContextManager
incompleteTypes map[entity.Key] *entity.Typedef
}
// Analyze takes in an AST and analyzes its contents within the context of this
// semantic tree. Analyzed entities will be added to the tree.
func (this *Tree) Analyze (ast parser.Tree) error {
func (this *Tree) Analyze (
unit uuid.UUID,
nicknames map[string] uuid.UUID,
ast fsplParser.Tree,
) error {
this.ensure()
this.ast = ast
this.resetRawMaps()
this.ast = ast
this.unit = unit
this.nicknames = nicknames
err := this.assembleRawMaps()
if err != nil { return err }
return this.analyzeDeclarations()
@ -29,72 +41,142 @@ func (this *Tree) Analyze (ast parser.Tree) error {
func (this *Tree) assembleRawMaps () error {
for _, declaration := range this.ast.Declarations {
switch declaration.(type) {
switch declaration := declaration.(type) {
case *entity.Typedef:
ty := declaration.(*entity.Typedef)
err := this.rawNameUnique(ty.Pos, ty.Name)
key := entity.Key {
Unit: this.unit,
Name: declaration.Name,
}
err := this.topLevelNameAvailable(declaration.Position, key)
if err != nil { return err }
this.rawTypes[ty.Name] = ty
declaration.Methods = make(map[string] *entity.Method)
this.rawTypes[key] = declaration
case *entity.Function:
function := declaration.(*entity.Function)
err := this.rawNameUnique (
function.Pos,
function.Signature.Name)
key := entity.Key {
Unit: this.unit,
Name: declaration.Signature.Name,
}
err := this.topLevelNameAvailable(declaration.Position, key)
if err != nil { return err }
this.rawFunctions[function.Signature.Name] = function
this.rawFunctions[key] = declaration
case *entity.Method:
method := declaration.(*entity.Method)
name := method.TypeName + "." + method.Signature.Name
err := this.rawNameUnique(method.Pos, name)
key := entity.Key {
Unit: this.unit,
Name: declaration.TypeName,
Method: declaration.Signature.Name,
}
err := this.topLevelNameAvailable(declaration.Position, key)
if err != nil { return err }
this.rawMethods[name] = method
this.rawMethods[key] = declaration
}
}
return nil
}
func (this *Tree) rawNameUnique (currentPos lexer.Position, name string) error {
var pos lexer.Position
var unique = true
if ty, isType := this.rawTypes[name]; isType {
pos = ty.Pos
unique = false
func (this *Tree) topLevelNameAvailable (currentPos errors.Position, key entity.Key) error {
if _, isPrimitive := primitiveTypes[key.Name]; isPrimitive {
return errors.Errorf (
currentPos, "cannot shadow primitive %s",
key.Name)
}
if function, isFunction := this.rawFunctions[name]; isFunction {
pos = function.Pos
unique = false
if _, isBuiltin := builtinTypes[key.Name]; isBuiltin {
return errors.Errorf (
currentPos, "cannot shadow builtin %s",
key.Name)
}
if method, isMethod := this.rawMethods[name]; isMethod {
pos = method.Pos
unique = false
}
if unique {
return nil
} else {
return participle.Errorf (
if ty, isType := this.rawTypes[key]; isType {
return errors.Errorf (
currentPos, "%s already declared at %v",
name, pos)
key.Name, ty.Position)
}
if function, isFunction := this.rawFunctions[key]; isFunction {
return errors.Errorf (
currentPos, "%s already declared at %v",
key.Name, function.Position)
}
if method, isMethod := this.rawMethods[key]; isMethod {
return errors.Errorf (
currentPos, "%s.%s already declared at %v",
key.Name, key.Method, method.Position)
}
return nil
}
func (this *Tree) analyzeDeclarations () error {
for name := range this.rawTypes {
ty, err := this.analyzeTypedef(name)
for key, rawType := range this.rawTypes {
ty, err := this.analyzeTypedef(rawType.Position, key, false)
if err != nil { return err }
this.Types[key] = ty
}
for key, rawFunction := range this.rawFunctions {
_, err := this.analyzeFunction(rawFunction.Position, key)
if err != nil { return err }
}
for _, rawMethod := range this.rawMethods {
_, err := this.analyzeMethod (
rawMethod.Position,
entity.Key {
Unit: this.unit,
Name: rawMethod.TypeName,
Method: rawMethod.Signature.Name,
})
if err != nil { return err }
this.Types[name] = ty
}
// TODO functions
// TODO methods
return nil
}
func (this *Tree) resolveNickname (pos errors.Position, nickname string) (uuid.UUID, error) {
if nickname == "" { return this.unit, nil }
fail := func () (uuid.UUID, error) {
return uuid.UUID { }, errors.Errorf (
pos, "no unit named %s",
nickname)
}
if this.nicknames == nil { return fail() }
unit, ok := this.nicknames[nickname]
if !ok { return fail() }
return unit, nil
}
func (this *Tree) ensure () {
if this.rawTypes != nil { return }
this.rawTypes = make(map[string] *entity.Typedef)
this.rawFunctions = make(map[string] *entity.Function)
this.rawMethods = make(map[string] *entity.Method)
this.Types = make(map[string] entity.Type)
this.Functions = make(map[string] *entity.Function)
this.resetRawMaps()
this.incompleteTypes = make(map[entity.Key] *entity.Typedef)
this.Types = make(map[entity.Key] *entity.Typedef)
this.Functions = make(map[entity.Key] *entity.Function)
for name, ty := range builtinTypes {
// builtin types have a zero UUID in their key
this.Types[entity.Key { Name: name }] = &entity.Typedef {
Acc: entity.AccessPublic,
Name: name,
Type: ty,
}
}
}
func (this *Tree) resetRawMaps () {
if this.rawTypes != nil &&
this.rawFunctions != nil &&
this.rawMethods != nil {
if len(this.rawTypes) > 0 {
this.rawTypes = make(map[entity.Key] *entity.Typedef)
}
if len(this.rawFunctions) > 0 {
this.rawFunctions = make(map[entity.Key] *entity.Function)
}
if len(this.rawMethods) > 0 {
this.rawMethods = make(map[entity.Key] *entity.Method)
}
} else {
this.rawTypes = make(map[entity.Key] *entity.Typedef)
this.rawFunctions = make(map[entity.Key] *entity.Function)
this.rawMethods = make(map[entity.Key] *entity.Method)
}
}

View File

@ -1,100 +1,278 @@
package analyzer
import "fmt"
import "github.com/alecthomas/participle/v2"
import "git.tebibyte.media/sashakoshka/fspl/entity"
import "git.tebibyte.media/fspl/fspl/errors"
import "git.tebibyte.media/fspl/fspl/entity"
func (this *Tree) analyzeTypedef (pos lexer.Position, name string) (entity.Type, error) {
if ty, exists := this.Types[name]; exists {
return ty
func (this *Tree) analyzeTypedef (
pos errors.Position,
key entity.Key,
acceptIncomplete bool,
) (
*entity.Typedef,
error,
) {
// return a builtin if it exists (search types with zero uuid)
if definition, exists := this.Types[entity.Key { Name: key.Name }]; exists {
return definition, nil
}
// return if exists already
if definition, exists := this.Types[key]; exists {
return definition, nil
}
definition, exists := this.RawTypes[name]
if !exists {
return participle.Errorf(pos, "no type named %s", name)
// check if incomplete
if definition, incomplete := this. incompleteTypes[key]; incomplete {
if acceptIncomplete {
return definition, nil
} else {
return nil, errors.Errorf (
pos, "type %s cannot be used in this context",
key.Name)
}
}
return this.analyzeType(definition.Type)
// analyze if it still needs to be analyzed
if definition, exists := this.rawTypes[key]; exists {
// update unit
definition.Unit = key.Unit
// analyze
var err error
definition.Type, err = this.analyzeTypeInternal (
definition.Type, definition, false)
return definition, err
}
// if we couldn't get the type, error
return nil, errors.Errorf(pos, "no type named %s", key.Name)
}
func (this *Tree) analyzeType (ty entity.Type) (entity.Type, error) {
if ty == entity.Void() { return ty }
var err error
func (this *Tree) analyzeType (
ty entity.Type,
acceptIncomplete bool,
) (
entity.Type,
error,
) {
return this.analyzeTypeInternal (ty, nil, acceptIncomplete)
}
switch ty.(type) {
case *entity.TypeNamed:
ty := ty.(*entity.TypeNamed)
defined, err := this.analyzeTypedef(ty.Pos, ty.Name)
ty.Type = defined
return ty, err
case *entity.TypePointer:
ty := ty.(*entity.TypePointer)
ty.Referenced, err = this.analyzeType(ty.Referenced)
return ty, err
case *entity.TypeSlice:
ty := ty.(*entity.TypeSlice)
ty.Element, err = this.analyzeType(ty.Element)
return ty, err
case *entity.TypeArray:
ty := ty.(*entity.TypeArray)
if ty.Length < 1 {
return ty, participle.Errorf (
ty.Pos, "array length must be > 1", name)
func (this *Tree) analyzeTypeInternal (
ty entity.Type,
root *entity.Typedef,
acceptIncomplete bool,
) (
entity.Type,
error,
) {
// edge cases
if ty == nil { return nil, nil }
var err error
// if we are analyzing a typedef, we will need to update incomplete type
// information as we go in order for recursive type definitions to work
rootKey := func () entity.Key {
return entity.Key {
Unit: this.unit,
Name: root.Name,
}
ty.Element, err = this.analyzeType(ty.Element)
}
updateIncompleteInfo := func () {
if root != nil { this.incompleteTypes[rootKey()] = root }
}
updatePseudoCompleteInfo := func () {
if root != nil { this.Types[rootKey()] = root }
}
if root != nil { defer delete(this.incompleteTypes, rootKey()) }
// ---------
// determine the access permission for the type
access := entity.AccessPublic; if root != nil {
access = root.Acc
}
switch ty := ty.(type) {
// named type
case *entity.TypeNamed:
ty.Unt = this.unit
ty.Acc = access
updateIncompleteInfo()
// resolve nickname of the unit where the typedef is
if primitive, isPrimitive := primitiveTypes[ty.Name]; isPrimitive {
return primitive, nil
}
unit, err := this.resolveNickname(ty.Position, ty.UnitNickname)
if err != nil { return nil, err }
// analyze the typedef
def, err := this.analyzeTypedef(ty.Position, entity.Key {
Unit: unit,
Name: ty.Name,
}, acceptIncomplete)
if err != nil { return nil, err }
ty.Type = def.Type
// check access permissions
err = this.typePrivate(ty.Position, ty)
if err != nil { return nil, err }
return ty, nil
// pointer type
case *entity.TypePointer:
ty.Unt = this.unit
ty.Acc = access
updateIncompleteInfo()
ty.Referenced, err = this.analyzeType(ty.Referenced, true)
return ty, err
// slice type
case *entity.TypeSlice:
ty.Unt = this.unit
ty.Acc = access
updateIncompleteInfo()
ty.Element, err = this.analyzeType(ty.Element, false)
return ty, err
// array type
case *entity.TypeArray:
ty.Unt = this.unit
ty.Acc = access
updateIncompleteInfo()
if ty.Length < 1 {
return ty, errors.Errorf (
ty.Position, "array length must be > 0")
}
ty.Element, err = this.analyzeType(ty.Element, false)
return ty, err
// struct type
case *entity.TypeStruct:
ty := ty.(*entity.TypeStruct)
ty.Unt = this.unit
ty.Acc = access
ty, err = this.assembleStructMap(ty)
if err != nil { return err }
updateIncompleteInfo()
if err != nil { return ty, err }
for name, member := range ty.MemberMap {
ty.MemberMap[name], err = this.analyzeType(member.Type)
ty.MemberMap[name].Ty,
err = this.analyzeType(member.Ty, false)
if err != nil { return ty, err }
}
return ty, nil
// interface type
case *entity.TypeInterface:
ty := ty.(*entity.TypeInterface)
ty.Unt = this.unit
ty.Acc = access
ty, err = this.assembleInterfaceMap(ty)
if err != nil { return err }
for name, method := range ty.MethodMap {
ty.MethodMap[name], err = this.analyzeSignature(method)
updatePseudoCompleteInfo()
if err != nil { return ty, err }
for name, behavior := range ty.BehaviorMap {
ty.BehaviorMap[name], err = this.analyzeBehavior(behavior)
if err != nil { return ty, err }
}
return ty, nil
// integer type
case *entity.TypeInt:
ty.Unt = this.unit
ty.Acc = access
updateIncompleteInfo()
if ty.Width < 1 {
return ty, errors.Errorf (
ty.Position, "integer width must be > 0")
}
return ty, nil
// floating point and word types
case *entity.TypeFloat:
ty.Unt = this.unit
ty.Acc = access
return ty, nil
case *entity.TypeWord:
ty.Unt = this.unit
ty.Acc = access
return ty, nil
default: panic(fmt.Sprint("BUG: analyzer doesnt know about type ", ty))
}
}
// typePrivate checks if the given type is private.
func (this *Tree) typePrivate (pos errors.Position, ty entity.Type) error {
if ty == nil { return nil }
original := ty
if named, ok := ty.(*entity.TypeNamed); ok {
ty = named.Type
}
if ty.Unit() != this.unit && ty.Access() == entity.AccessPrivate {
return errors.Errorf(pos, "type %v is private", entity.FormatType(original))
}
return nil
}
// typeRestricted checks if the given type is restricted or private.
func (this *Tree) typeRestricted (pos errors.Position, ty entity.Type) error {
if ty == nil { return nil }
err := this.typePrivate(pos, ty)
if err != nil { return err }
original := ty
if named, ok := ty.(*entity.TypeNamed); ok {
ty = named.Type
}
if ty.Unit() != this.unit && ty.Access() == entity.AccessRestricted {
return errors.Errorf(pos, "type %v is restricted", entity.FormatType(original))
}
return nil
}
func (this *Tree) assembleStructMap (ty *entity.TypeStruct) (*entity.TypeStruct, error) {
ty.MemberMap = make(map[string] *entity.Declaration)
ty.MemberOrder = make([]string, len(ty.Members))
for index, member := range ty.Members {
if previous, exists := ty.MemberMap[member.Name]; exists {
return ty, participle.Errorf (
member.Pos, "%s already listed in struct at %v",
member.Name, previous.Pos)
return ty, errors.Errorf (
member.Position, "%s already listed in struct at %v",
member.Name, previous.Position)
}
ty.MemberMap [member.Name] = member
ty.MemberOrder[index] = member.Name
ty.Members [index] = member
}
ty.Members = nil
return ty, nil
}
func (this *Tree) assembleInterfaceMap (ty *entity.TypeStruct) (*entity.TypeStruct, error) {
ty.MethodMap = make(map[string] *entity.Signature)
ty.MethodOrder = make([]string, len(ty.Methods))
for index, method := range ty.Methods {
if previous, exists := ty.MethodMap[method.Name]; exists {
return ty, participle.Errorf (
method.Pos, "%s already listed in interface at %v",
method.Name, previous.Pos)
func (this *Tree) assembleInterfaceMap (ty *entity.TypeInterface) (*entity.TypeInterface, error) {
ty.BehaviorMap = make(map[string] *entity.Signature)
ty.BehaviorOrder = make([]string, len(ty.Behaviors))
for index, method := range ty.Behaviors {
if previous, exists := ty.BehaviorMap[method.Name]; exists {
return ty, errors.Errorf (
method.Position, "%s already listed in interface at %v",
method.Name, previous.Position)
}
ty.MethodMap [method.Name] = member
ty.MethodOrder[index] = method.Name
ty.BehaviorMap [method.Name] = method
ty.BehaviorOrder[index] = method.Name
ty.Behaviors [index] = method
}
ty.Methods = nil
return ty, nil
}
func (this *Tree) analyzeBehavior (behavior *entity.Signature) (*entity.Signature, error) {
behavior, err := this.assembleSignatureMap(behavior)
behavior.Unt = this.unit
if err != nil { return behavior, nil }
for name, argument := range behavior.ArgumentMap {
behavior.ArgumentMap[name].Ty, err = this.analyzeType(argument.Ty, false)
if err != nil { return behavior, err }
}
behavior.Return, err = this.analyzeType(behavior.Return, false)
return behavior, err
}

180
analyzer/type_test.go Normal file
View File

@ -0,0 +1,180 @@
package analyzer
import "testing"
func TestTypedefUniqueErr (test *testing.T) {
testStringErr (test,
"Hello already declared at stream0.fspl:2:1", 3, 1,
`
Hello: *Int
Hello: (. x:Int y:Int)
`)
}
func TestTypedefUniqueErrShadowPrimitive (test *testing.T) {
testStringErr (test,
"cannot shadow primitive U8", 2, 1,
`
U8: Int
`)
}
func TestTypedefUniqueErrShadowBuiltin (test *testing.T) {
testStringErr (test,
"cannot shadow builtin String", 2, 1,
`
String: Int
`)
}
func TestTypedefUnique (test *testing.T) {
testString (test,
`
Hello: *Int
World: (. x:Int y:Int)
`)
}
func TestTypedefRecursiveErr (test *testing.T) {
testStringErr (test,
"type List cannot be used in this context", 4, 9,
`
List: (.
value: Int
next: List)
`)
}
func TestTypedefRecursive (test *testing.T) {
testString (test,
`
List: (.
value: Int
next: *List)
`)
}
func TestTypeNamedErrMissing (test *testing.T) {
testStringErr (test,
"no type named Missing", 2, 10,
`
Present: Missing
`)
}
func TestTypeNamed (test *testing.T) {
testString (test,
`
Example: Int
AllBuiltin: (.
int: Int
uint: UInt
byte: Byte
rune: Rune
string: String
i8: I8
i16: I16
i32: I32
i64: I64
u8: U8
u16: U16
u32: U32
u64: U64
f32: F32
f64: F64
bool: Bool)
[main] = {
example:Example
allBuiltin:AllBuiltin
}
`)
}
func TestTypePointer (test *testing.T) {
testString (test,
`
Ptr: *Int
PtrPtr: **Int
PtrPtrPtr: *PtrPtr
ArrPtr: *5:Int
SlicePtr: **:Int
`)
}
func TestTypeSlice (test *testing.T) {
testString (test,
`
Slice: *:Int
RaggedSlice: *:*:Int
`)
}
func TestTypeArrayErrSizeZero (test *testing.T) {
testStringErr (test,
"array length must be > 0", 2, 8,
`
Array: 0:Int
`)
}
func TestTypeArray (test *testing.T) {
testString (test,
`
Array: 5:Int
Matrix: 5:6:Int
`)
}
func TestTypeStructMemberUniqueErr (test *testing.T) {
testStringErr (test,
"x already listed in struct at stream0.fspl:3:2", 6, 2,
`
Bird: (.
x:Int
y:Int
z:Int
x:U8)
`)
}
func TestTypeStructMemberUnique (test *testing.T) {
testString (test,
`
Bird: (.
x:Int
y:Int
z:Int
a:U8)
`)
}
func TestTypeInterfaceBehaviorUniqueErr (test *testing.T) {
testStringErr (test,
"fly already listed in interface at stream0.fspl:2:10", 2, 16,
`
Bird: (~ [fly] [fly])
`)
}
func TestTypeInterfaceBehaviorUnique (test *testing.T) {
testString (test,
`
Bird: (~ [fly] [land])
`)
}
func TestTypeIntegerLiteralVoid (test *testing.T) {
testStringErr (test,
"cannot use integer literal as Void", 2, 10,
`
[main] = 5
`)
}
func TestTypeStringLiteralVoid (test *testing.T) {
testStringErr (test,
"cannot use string literal as Void", 2, 10,
`
[main] = 'hello'
`)
}

122
assets/fspl.svg Normal file
View File

@ -0,0 +1,122 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="1024"
height="1024"
viewBox="0 0 1024 1024"
version="1.1"
id="svg1"
inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
sodipodi:docname="fspl.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#FF0000"
bordercolor="#ffffff"
borderopacity="1"
inkscape:showpageshadow="false"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#646464"
inkscape:document-units="mm"
labelstyle="default"
showgrid="true"
inkscape:zoom="0.69531497"
inkscape:cx="342.29092"
inkscape:cy="574.55976"
inkscape:window-width="1886"
inkscape:window-height="1000"
inkscape:window-x="16"
inkscape:window-y="1119"
inkscape:window-maximized="0"
inkscape:current-layer="layer1">
<inkscape:grid
id="grid1"
units="px"
originx="0"
originy="0"
spacingx="32"
spacingy="32"
empcolor="#0099e5"
empopacity="0.30196078"
color="#0099e5"
opacity="0.14901961"
empspacing="4"
dotted="false"
gridanglex="30"
gridanglez="30"
visible="true" />
</sodipodi:namedview>
<defs
id="defs1">
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect6"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,33.866667,0,1 @ F,0,0,1,0,33.866667,0,1 @ F,0,0,1,0,0,0,1"
radius="128"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect5"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,33.866667,0,1 @ F,0,0,1,0,33.866667,0,1 @ F,0,0,1,0,33.866667,0,1 @ F,0,0,1,0,33.866667,0,1 @ F,0,0,1,0,33.866667,0,1 @ F,0,0,1,0,33.866667,0,1 @ F,0,0,1,0,33.866667,0,1 @ F,0,0,1,0,0,0,1"
radius="128"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<path
style="fill:none;stroke:#FF0000;stroke-width:48;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill"
d="M 592.50809,272 H 369.86667 A 33.866667,33.866667 135 0 0 336,305.86667 v 28.26666 96 96 A 33.866667,33.866667 45 0 0 369.86667,560 H 654.13333 A 33.866667,33.866667 45 0 1 688,593.86667 v 28.26666 96 A 33.866667,33.866667 135 0 1 654.13333,752 H 336"
id="path1"
inkscape:path-effect="#path-effect5"
inkscape:original-d="M 592.50809,272 H 336 v 96 96 96 h 352 v 96 96 H 336"
sodipodi:nodetypes="ccccccccc" />
<path
style="fill:none;stroke:#FF0000;stroke-width:48;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill;stroke-linejoin:miter"
d="M 336.03852,368.00001 H 495.98905"
id="path2" />
<path
style="fill:none;stroke:#FF0000;stroke-width:48;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill"
d="M 336.02172,464 H 623.97828"
id="path3" />
<path
style="fill:none;stroke:#FF0000;stroke-width:48;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill"
d="M 688,656 H 369.86667 A 33.866667,33.866667 135 0 0 336,689.86667 V 814.13333 A 33.866667,33.866667 45 0 0 369.86667,848 h 257.05452"
id="path4"
inkscape:path-effect="#path-effect6"
inkscape:original-d="M 688,656 H 336 v 192 h 290.92119"
sodipodi:nodetypes="cccc" />
<path
style="fill:none;stroke:#FF0000;stroke-width:48;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill;stroke-linejoin:miter"
d="M 560.08272,176 H 463.94476"
id="path5" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

418
cli/cli.go Normal file
View File

@ -0,0 +1,418 @@
package cli
import "os"
import "io"
import "fmt"
import "sort"
import "errors"
import "strings"
import "strconv"
// Cli represents a command line interface. It is not tied to os.Args, and it is
// possible to create multiple Cli's to have so many sub-commands in one
// program.
type Cli struct {
Logger
// Description describes the application.
Description string
// The syntax description of the command. It appears in Usage after the
// name of the program (os.Args[0]). If not specified, "[OPTIONS]..."
// will be used instead.
Syntax string
// Flag is a set of flags to parse.
Flags []*Flag
// Args is a list of leftover arguments that were not parsed.
Args []string
// Sub contains sub-commands.
Sub map[string] *Cli
// Super contains the string of commands used to invoke this Cli.
// For example, a command invoked using
// <program> foo bar
// Would have Super set to
// "foo bar"
// This is automatically filled out by Cli.AddSub.
Super string
}
// New creates a new Cli from a command description and a set of flags. These
// flags should be created as variables before passing them to New(), so that
// their values can be extracted after Parse is called.
func New (description string, flags ...*Flag) *Cli {
return &Cli {
Description: description,
Flags: flags,
Sub: make(map[string] *Cli),
}
}
// AddSub adds a sub-command to this command. It automatically fills out the
// sub-command's Super field, and alters the prefix of its Logger to match.
func (this *Cli) AddSub (name string, sub *Cli) {
super := this.Super
if super != "" { super += " " }
super += name
sub.Super = super
sub.Prefix = os.Args[0] + " " + super
this.Sub[name] = sub
}
// Parse parses the given set of command line arguments according to the flags
// defined within the Cli. The first element of the argument slice is always
// dropped, because it is assumed that it just contains the command name. If the
// second element matches up with the name of a sub-command, parsing is
// deferred to that sub-command instead. The sub-command will be given all but
// the first element of args. This method will return the address of the
// command that did the parsing.
func (this *Cli) Parse (args []string) (*Cli, error) {
// try to find a sub-command
if this.Sub != nil && len(args) > 1 {
if sub, ok := this.Sub[args[1]]; ok {
return sub.Parse(args[1:])
}
}
// strip out program name
if len(args) > 0 { args = args[1:] }
next := func () string {
args = args[1:]
if len(args) > 0 {
return args[0]
} else {
return ""
}
}
processFlag := func (flag *Flag, allowInput bool) error {
if flag.Validate == nil {
flag.Value = "true"
} else {
if len(args) < 1 || !allowInput {
return errors.New(
flag.String() +
" requires a value")
}
value := next()
err := flag.Validate(value)
if err != nil {
return errors.New(fmt.Sprint (
"bad value for ", flag.String(), ": ",
err))
}
flag.Value = value
}
if flag.Found != nil { flag.Found(this, flag.Value) }
return nil
}
// process args one by one
for ; len(args) > 0; next() {
arg := args[0]
switch {
case arg == "--":
// halt parsing
this.Args = append(this.Args, args[1:]...)
return nil, nil
case strings.HasPrefix(arg, "--"):
// long flag
flag, ok := this.LongFlag(arg[2:])
if !ok {
return nil, errors.New("unknown flag: " + arg)
}
err := processFlag(flag, true)
if err != nil { return nil, err }
case strings.HasPrefix(arg, "-") && len(arg) > 1:
// one or more short flags
arg := arg[1:]
for _, part := range arg {
flag, ok := this.ShortFlag(part)
if !ok {
return nil, errors.New (
"unknown flag: -" +
string(part))
}
err := processFlag(flag, len(arg) == 1)
if err != nil { return nil, err }
}
default:
// not a flag
this.Args = append(this.Args, arg)
}
}
return this, nil
}
// ParseOrExit is like Parse, but if an error occurs while parsing the argument
// list, it prints out Usage and exits with error status 2.
func (this *Cli) ParseOrExit (args []string) *Cli {
command, err := this.Parse(args)
if err != nil {
fmt.Fprintf(this, "%s: %v\n\n", os.Args[0], err)
this.Usage()
os.Exit(2)
}
return command
}
// ShortFlag searches for and returns the flag with the given short form. If it
// was found, it returns true. If it was not found, it returns nil, false.
func (this *Cli) ShortFlag (short rune) (*Flag, bool) {
if short == 0 { return nil, false }
for _, flag := range this.Flags {
if flag.Short == short { return flag, true }
}
return nil, false
}
// LongFlag searches for and returns the flag with the given long form. If it
// was found, it returns true. If it was not found, it returns nil, false.
func (this *Cli) LongFlag (long string) (*Flag, bool) {
if long == "" { return nil, false }
for _, flag := range this.Flags {
if flag.Long == long { return flag, true }
}
return nil, false
}
// Usage prints out usage/help information.
func (this *Cli) Usage () {
// syntax
fmt.Fprint(this, "Usage:")
fmt.Fprint(this, " ", os.Args[0])
if this.Super != "" {
fmt.Fprint(this, " ", this.Super)
}
hasSubCommands := this.Sub != nil && len(this.Sub) > 0
if hasSubCommands {
fmt.Fprint(this, " [COMMAND]")
}
if this.Syntax == "" {
fmt.Fprint(this, " [OPTION]...")
} else {
fmt.Fprint(this, " ", this.Syntax)
}
fmt.Fprint(this, "\n\n")
// description
if this.Description != "" {
fmt.Fprintf(this, "%s\n\n", this.Description)
}
// fit the longest flag
longest := 0
for _, flag := range this.Flags {
if len(flag.Long) > longest {
longest = len(flag.Long)
}
}
format := fmt.Sprint("\t%-", longest + 8, "s%s\n")
// flags
for _, flag := range this.Flags {
shortLong := ""
if flag.Short == 0 {
shortLong += " "
} else {
shortLong += "-" + string(flag.Short)
}
if flag.Short != 0 && flag.Long != "" {
shortLong += ", "
}
if flag.Long != "" {
shortLong += "--" + flag.Long
}
fmt.Fprintf(this, format, shortLong, flag.Help)
}
// commands
if hasSubCommands {
fmt.Fprint(this, "\nCommands:\n\n")
names := sortMapKeys(this.Sub)
// fit the longest command
longest := 0
for _, name := range names {
if len(name) > longest {
longest = len(name)
}
}
format := fmt.Sprint("\t%-", longest + 2, "s%s\n")
for _, name := range names {
fmt.Fprintf(this, format, name, this.Sub[name].Description)
}
}
}
// Logger prints messages to an output writer.
type Logger struct {
// Writer specifies the writer to output to. If it is nil, the Logger
// will output to os.Stderr. If you wish to silence the output, set it
// to io.Discard.
io.Writer
// Debug determines whether or not debug messages will be logged.
Debug bool
// Prefix is printed before all messages. If this is an empty string,
// os.Args[0] will be used.
Prefix string
}
// Println logs a normal message.
func (this *Logger) Println (v ...any) {
fmt.Fprintf(this, "%s: %s", this.prefix(), fmt.Sprintln(v...))
}
// Errorln logs an error message.
func (this *Logger) Errorln (v ...any) {
fmt.Fprintf(this, "%s: %s", this.prefix(), fmt.Sprintln(v...))
}
// Warnln logs a warning.
func (this *Logger) Warnln (v ...any) {
fmt.Fprintf(this, "%s: warning: %s", this.prefix(), fmt.Sprintln(v...))
}
// Debugln logs debugging information. It will only print the message if the
// Debug field is set to true.
func (this *Logger) Debugln (v ...any) {
if !this.Debug { return }
fmt.Fprintf(this, "%s: debug: %s", this.prefix(), fmt.Sprintln(v...))
}
func (this *Logger) prefix () string {
if this.Prefix == "" {
return os.Args[0]
} else {
return this.Prefix
}
}
// Write writes to the Logger's writer. If it is nil, it writes to os.Stderr.
func (this *Logger) Write (data []byte) (n int, err error) {
if this.Writer == nil {
return os.Stderr.Write(data)
} else {
return this.Writer.Write(data)
}
}
// Flag is a command line option.
type Flag struct {
Short rune // The short form of the flag (-l)
Long string // Long form of the flag (--long-form)
Help string // Help text to display by the flag
// Validate is an optional function that is called when an input is
// passed to the flag. It checks whether or not the input is valid, and
// returns an error if it isn't. If this function is nil, the flag is
// assumed to have no input.
Validate func (string) error
// Value contains the input given to the flag, and is filled out when
// the flags are parsed. If this is a non-input flag (if Validate is
// nil), this will be set to true. If the flag was not specified, Value
// will be unchanged.
Value string
// Found is an optional function that is called each time the flag is
// found, and has a valid argument (if applicable). The value is passed
// to the function, as well as the command that it was found in.
Found func (*Cli, string)
}
// String returns --<LongForm> if specified, and if not returns -<ShortForm>
func (this *Flag) String () string {
if this.Long == "" {
return fmt.Sprintf("-%c", this.Short)
} else {
return fmt.Sprintf("--%s", this.Long)
}
}
// NewFlag creates a new flag that does not take in a value.
func NewFlag (short rune, long string, help string) *Flag {
return &Flag {
Short: short,
Long: long,
Help: help,
}
}
// NewInputFlag creates a new flag that does take in a value. This function will
// panic if the given validation function is nil.
func NewInputFlag (short rune, long string, help string, defaul string, validate func (string) error) *Flag {
if validate == nil {
panic("validate must be non-nil for a flag to take in a value")
}
return &Flag {
Short: short,
Long: long,
Help: help,
Value: defaul,
Validate: validate,
}
}
// NewHelp creates a new help flag activated by --help or -h. It shows help
// information for the command that it is a part of, and causes the program to
// exit with a status of 2.
func NewHelp () *Flag {
flag := NewFlag('h', "help", "Display usage information and exit")
flag.Found = func (command *Cli, value string) {
command.Usage()
os.Exit(2)
}
return flag
}
// ValString is a validation function that always returns nil, accepting any
// string.
func ValString (value string) error {
return nil
}
// ValInt is a validation function that returns an error if the value is not an
// integer.
func ValInt (value string) error {
_, err := strconv.Atoi(value)
return err
}
// NewValSet returns a validation function that accepts a set of strings.
func NewValSet (allowed ...string) func (value string) error {
return func (value string) error {
allowedFmt := ""
for index, test := range allowed {
if test == value { return nil }
if index > 0 { allowedFmt += ", " }
allowedFmt += test
}
return errors.New (
"value must be one of (" + allowedFmt + ")")
}
}
func sortMapKeys[T any] (unsorted map[string] T) []string {
keys := make([]string, len(unsorted))
index := 0
for key := range unsorted {
keys[index] = key
index ++
}
sort.Strings(keys)
return keys
}

15
cmd/fsplc/doc.go Normal file
View File

@ -0,0 +1,15 @@
// fsplc compiles FSPL programs.
//
// Its job is to take in FSPL code, and compile it to one of the supported
// output formats. Currently it supports native object files, native ASM, and
// LLVM IR code.
//
// Usage: fsplc [OPTION]... ADDRESS
//
// -h, --help Display usage information and exit
// --debug Print extra debug information while compiling
// -q, --quiet Don't print warnings, errors, etc.
// -m, --format Output format (.s, .o, .ll)
// -o, --output Output filename
// -O, --optimization Optimization level (0-3)
package main

79
cmd/fsplc/main.go Normal file
View File

@ -0,0 +1,79 @@
package main
import "os"
import "io"
import "path/filepath"
import "git.tebibyte.media/fspl/fspl/cli"
import "git.tebibyte.media/fspl/fspl/entity"
import "git.tebibyte.media/fspl/fspl/compiler"
import ferrors "git.tebibyte.media/fspl/fspl/errors"
func main () {
// instantiate the compiler
// FIXME: perhaps we want different paths on Windows?
comp := new(compiler.Compiler)
comp.Writer = os.Stderr
comp.Resolver = compiler.NewResolver (
"/usr/local/src/fspl",
"/usr/src/fspl",
"/usr/local/incude/fspl",
"/usr/include/fspl")
homeDir, err := os.UserHomeDir()
if err != nil {
comp.Errorln(err)
os.Exit(2)
}
comp.Resolver.AddPathFront (
filepath.Join(homeDir, ".local/src/fspl"),
filepath.Join(homeDir, ".local/include/fspl"))
// take in CLI flags
debug := cli.NewFlag (
0, "debug",
"Print extra debug information while compiling")
quiet := cli.NewFlag (
'q', "quiet",
"Don't print warnings, errors, etc.")
format := cli.NewInputFlag (
'm', "format",
"Output format (.s, .o, .ll)", "",
cli.NewValSet(".s", ".o", ".ll"))
output := cli.NewInputFlag (
'o', "output",
"Output filename", "",
cli.ValString)
optimization := cli.NewInputFlag (
'O', "optimization",
"Optimization level (0-3)", "0",
cli.NewValSet("0", "1", "2", "3"))
application := cli.New (
"Compile FSPL source files",
cli.NewHelp(),
debug,
quiet,
format,
output,
optimization)
application.Syntax = "[OPTION]... ADDRESS"
application.ParseOrExit(os.Args)
if len(application.Args) != 1 {
comp.Errorln("please specify one unit address")
application.Usage()
os.Exit(2)
}
// configure the compiler based on user input
comp.Output = output.Value
comp.Optimization = optimization.Value
comp.Format = format.Value
comp.Debug = debug.Value != ""
if quiet.Value != "" { comp.Writer = io.Discard }
err = comp.CompileUnit(entity.Address(application.Args[0]))
if err != nil {
comp.Errorln(ferrors.Format(err))
os.Exit(1)
}
}

10
cmd/fsplmod/doc.go Normal file
View File

@ -0,0 +1,10 @@
// fsplmod manages FSPL modules.
// Usage: fsplmod [COMMAND] [OPTION]...
//
// -h, --help Display usage information and exit
//
// Commands:
//
// bump Re-generate the UUID of a module
// new Create a new module in the specified directory
package main

107
cmd/fsplmod/main.go Normal file
View File

@ -0,0 +1,107 @@
package main
import "io"
import "os"
import "path/filepath"
import "github.com/google/uuid"
import "git.tebibyte.media/fspl/fspl/cli"
import "git.tebibyte.media/fspl/fspl/lexer"
import "git.tebibyte.media/fspl/fspl/entity"
import "git.tebibyte.media/fspl/fspl/parser/meta"
import ferrors "git.tebibyte.media/fspl/fspl/errors"
func main () {
// CLI: application
applicationCli := cli.New (
"Manage FSPL modules",
cli.NewHelp())
// CLI: new
newCli := cli.New (
"Create a new module in the specified directory",
cli.NewHelp())
newCli.Syntax = "[OPTION]... DIRECTORY"
applicationCli.AddSub("new", newCli)
// CLI: bump
bumpCli := cli.New (
"Re-generate the UUID of a module",
cli.NewHelp())
newCli.Syntax = "[OPTION]... DIRECTORY"
applicationCli.AddSub("bump", bumpCli)
handleErr := func (cli *cli.Cli, err error, code int) {
if err == nil { return }
cli.Errorln(ferrors.Format(err))
os.Exit(code)
}
switch cli := applicationCli.ParseOrExit(os.Args); cli {
case newCli:
// Create a new module in the specified directory
if len(cli.Args) != 1 {
cli.Errorln("please specify one module directory")
cli.Usage()
os.Exit(2)
}
dir := cli.Args[0]
_, err := os.Stat(dir)
if err != nil {
err := os.Mkdir(dir, 0755)
handleErr(cli, err, 1)
}
moduleId, err := uuid.NewRandom()
handleErr(cli, err, 1)
cli.Println("creating module", moduleId, "in", dir)
metadataPath := filepath.Join(dir, "fspl.mod")
file, err := os.OpenFile (
metadataPath,
os.O_CREATE | os.O_WRONLY | os.O_TRUNC,
0644)
handleErr(cli, err, 1)
defer file.Close()
meta := entity.Metadata {
UUID: moduleId,
}
_, err = io.WriteString(file, meta.String() + "\n")
handleErr(cli, err, 1)
case bumpCli:
// Re-generate the UUID of a module
if len(cli.Args) != 1 {
cli.Errorln("please specify one module directory")
cli.Usage()
os.Exit(2)
}
dir := cli.Args[0]
metadataPath := filepath.Join(dir, "fspl.mod")
file, err := os.OpenFile(metadataPath, os.O_RDWR, 0644)
handleErr(cli, err, 1)
defer file.Close()
lx, err := lexer.LexReader(metadataPath, file)
handleErr(cli, err, 1)
meta := metaParser.Tree { }
err = meta.Parse(lx)
handleErr(cli, err, 1)
newModuleId, err := uuid.NewRandom()
handleErr(cli, err, 1)
cli.Println("changing uuid of", dir + ":", meta.UUID, "->", newModuleId)
meta.UUID = newModuleId
file.Truncate(0)
file.Seek(0, 0)
_, err = io.WriteString(file, meta.String() + "\n")
handleErr(cli, err, 1)
default:
applicationCli.Usage()
os.Exit(2)
}
}

144
compiler/common_test.go Normal file
View File

@ -0,0 +1,144 @@
package compiler
import "io/fs"
import "embed"
import "strings"
import "testing"
import "os/exec"
import "path/filepath"
import "git.tebibyte.media/fspl/fspl/entity"
import "git.tebibyte.media/fspl/fspl/errors"
import "git.tebibyte.media/fspl/fspl/testcommon"
//go:embed all:test-data/*
var testData embed.FS
func defaultCompiler () *Compiler {
// instantiate and configure the compiler
comp := new(Compiler)
comp.Prefix = "compiler"
comp.Resolver = NewResolver (
"/test-data/usr/local/src/fspl",
"/test-data/usr/src/fspl",
"/test-data/usr/local/incude/fspl",
"/test-data/usr/include/fspl")
comp.Resolver.FS = testData
comp.Format = ".o"
comp.Debug = true
return comp
}
func compileDependency (
test *testing.T,
address entity.Address,
) string {
// create temporary directory
temp := test.TempDir()
// instantiate and configure the compiler
compOutputBuilder := new(strings.Builder)
comp := defaultCompiler()
defer func () {
test.Logf (
"COMPILER LOG (dependency %s):\n%s",
address, compOutputBuilder)
} ()
comp.Writer = compOutputBuilder
nickname, ok := address.Nickname()
if !ok {
test.Fatal("could not generate nickname for", address)
}
comp.Output = filepath.Join(temp, nickname)
// compile to object file
err := comp.CompileUnit(address)
if err != nil {
test.Fatal("compiler returned error:", errors.Format(err))
}
return comp.Output
}
func testUnit (
test *testing.T,
address entity.Address,
clangArgs []string,
stdin, stdout string,
exit int,
args ...string,
) {
// create temporary directory
temp := test.TempDir()
// instantiate and configure the compiler
compOutputBuilder := new(strings.Builder)
comp := defaultCompiler()
defer func () {
test.Log("COMPILER LOG (main unit):\n" + compOutputBuilder.String())
} ()
comp.Writer = compOutputBuilder
comp.Output = filepath.Join(temp, "output.o")
// compile to object file
err := comp.CompileUnit(address)
if err != nil {
test.Fatal("compiler returned error:", errors.Format(err))
}
// link the object file into an executable
executablePath := filepath.Join(temp, "output")
linkCommand := exec.Command("clang", append (
clangArgs,
comp.Output,
"-o",
executablePath)...)
linkCommand.Stdout = compOutputBuilder
linkCommand.Stderr = compOutputBuilder
test.Log("running link command: ", linkCommand)
err = linkCommand.Run()
if err != nil {
test.Fatal("error linking executable:", err)
}
// run the executable file and check its output
executableCommand := exec.Command(executablePath, args...)
stdoutBuilder := new(strings.Builder)
executableCommand.Stdin = strings.NewReader(stdin)
executableCommand.Stdout = stdoutBuilder
test.Log("running executable command: ", executableCommand)
err = executableCommand.Run()
// check error, compare exit code
if exitErr, ok := err.(*exec.ExitError); ok {
code := exitErr.ExitCode()
if code != exit {
test.Errorf (
"expecting exit code %d, got %d",
exit, code)
}
} else if err != nil {
test.Fatalf("error running %s: %v", executablePath, err)
} else {
if 0 != exit {
test.Errorf("expecting exit code %d, got 0", exit)
}
}
// compare stdout
gotStdout := stdoutBuilder.String()
if gotStdout != stdout {
testcommon.CompareHex(test, stdout, gotStdout)
test.Fail()
}
}
func TestEmbedOk (test *testing.T) {
err := fs.WalkDir(testData, ".", func (path string, d fs.DirEntry, err error) error {
test.Log("file:", path)
return err
})
if err != nil {
test.Error("walk dir failed: ", err)
}
}

158
compiler/compile-unit.go Normal file
View File

@ -0,0 +1,158 @@
package compiler
import "os"
import "fmt"
import "errors"
import "os/exec"
import "path/filepath"
import "git.tebibyte.media/fspl/fspl/llvm"
import "git.tebibyte.media/fspl/fspl/entity"
import "git.tebibyte.media/fspl/fspl/analyzer"
import "git.tebibyte.media/fspl/fspl/generator/native"
func (this *Compiler) CompileUnit (address entity.Address) error {
this.Debugln("using search path", this.Resolver.Path)
path, err := this.ResolveCwd(address)
if err != nil { return err }
this.Debugln("compiling unit", path)
var semanticTree analyzer.Tree
_, err = this.AnalyzeUnit(&semanticTree, path, false)
if err != nil { return err }
irModule, err := native.NativeTarget().Generate(semanticTree)
if err != nil {
return this.bug(err)
}
// if the format isn't specified, try to get it from the filename
// extension of the input. if that isn't specified, default to .o
if this.Format == "" {
this.Format = filepath.Ext(this.Output)
}
if this.Format == "" {
this.Format = ".o"
}
// if the output file is unspecified, generate a nickname from the
// input address. if that doesn't work, default to "output"
if this.Output == "" {
nickname, ok := address.Nickname()
if !ok { nickname = "output" }
this.Output = nickname + this.Format
}
// do something based on the output extension
// TODO: add .so
switch this.Format {
case ".s":
return this.CompileIRModule(irModule, "asm")
case ".o":
return this.CompileIRModule(irModule, "obj")
case ".ll":
file, err := os.Create(this.Output)
if err != nil { return err }
defer file.Close()
_, err = irModule.WriteTo(file)
return err
case "":
return errors.New(fmt.Sprint (
"output file has no extension, ",
"could not determine output type"))
default:
return errors.New(fmt.Sprintf (
"unknown output type %s", this.Format))
}
}
func (this *Compiler) CompileIRModule (module *llvm.Module, filetype string) error {
this.Debugln("compiling ir module to filetype", filetype)
commandName, args, err := this.FindBackend(filetype)
if err != nil { return err }
command := exec.Command(commandName, args...)
this.Debugln("compiling ir using:", command)
command.Stdout = os.Stdout
command.Stderr = os.Stderr
pipe, err := command.StdinPipe()
if err != nil { return err }
err = command.Start()
if err != nil { return err }
_, err = module.WriteTo(pipe)
if err != nil { return err }
pipe.Close()
return command.Wait()
}
// FindBackend returns the name of an LLVM backend command, and a list of
// arguments to pass to it. It tries commands in this order:
// - llc
// - llc-<latest> -> llc-14
// - clang
// If none were found, it returns an error.
func (this *Compiler) FindBackend (filetype string) (string, []string, error) {
optimization := "0"
if this.Optimization != "" { optimization = this.Optimization }
llcArgs := []string {
"-",
fmt.Sprintf("-filetype=%s", filetype),
"-o", this.Output,
fmt.Sprintf("-O=%s", optimization),
}
// attempt to use llc command
commandName := "llc"
_, err := exec.LookPath(commandName)
this.Debugln("trying", commandName)
if err == nil {
return commandName, llcArgs, nil
}
// attempt to find a versioned llc command, counting down from the
// latest known version
llcVersion := 17 // TODO change this number to the latest llc version in
// the future
for err != nil && llcVersion >= 14 {
commandName := fmt.Sprintf("llc-%d", llcVersion)
this.Debugln("trying", commandName)
_, err = exec.LookPath(commandName)
if err == nil {
if llcVersion == 14 {
// -opaque-pointers is needed in version 14
llcArgs = append(llcArgs, "-opaque-pointers")
this.Warnln (
"using llvm llc version 14, which",
"does not have proper support for",
"opaque pointers. expect bugs")
}
return commandName, llcArgs, nil
}
llcVersion --
}
// attempt to use clang
commandName = "clang"
_, err = exec.LookPath(commandName)
this.Debugln("trying", commandName)
if err == nil {
if filetype != "obj" {
return "", nil, errors.New("need 'llc' to compile to " + filetype)
}
this.Warnln("falling back to clang to compile llvm ir. expect bugs")
return commandName, []string {
"-c",
"-x", "ir",
"-o", this.Output,
fmt.Sprintf("-O%s", this.Optimization),
"-",
}, nil
}
return "", nil, errors.New (
"no suitable backends found: please make sure either 'llc' " +
"or 'clang' are accessable from your PATH")
}

248
compiler/compiler.go Normal file
View File

@ -0,0 +1,248 @@
package compiler
import "fmt"
import "sort"
import "io/fs"
import "errors"
import "path/filepath"
import "github.com/google/uuid"
import "git.tebibyte.media/fspl/fspl/cli"
import "git.tebibyte.media/fspl/fspl/lexer"
import "git.tebibyte.media/fspl/fspl/entity"
import "git.tebibyte.media/fspl/fspl/analyzer"
import "git.tebibyte.media/fspl/fspl/parser/fspl"
import "git.tebibyte.media/fspl/fspl/parser/meta"
import ferrors "git.tebibyte.media/fspl/fspl/errors"
type Compiler struct {
Resolver
cli.Logger
Output string
Optimization string
Format string
}
func (this *Compiler) bug (err error) error {
return errors.New(fmt.Sprintln (
"Bug detected in the compiler!\n" +
"The FSPL compiler has experienced an error that should not",
"happen.\n" +
"Please submit a report with this info and the code you were",
"compiling to:",
"https://git.tebibyte.media/sashakoshka/fspl/issues\n" +
"The error is:", err))
}
func (this *Compiler) AnalyzeUnit (
semanticTree *analyzer.Tree,
path string,
skim bool,
) (
uuid.UUID,
error,
) {
this.Debugln("entering unit analysis", path)
filePath, isFile := entity.Address(path).SourceFile()
modulePath, isModule := entity.Address(path).Module()
if !isFile && !isModule {
return uuid.UUID { }, errors.New(fmt.Sprintf (
"%v is not a module, nor a source file",
path))
}
if isModule {
return this.AnalyzeModule(semanticTree, modulePath, skim)
} else {
return this.AnalyzeSourceFile(semanticTree, filePath, skim)
}
}
func (this *Compiler) AnalyzeModule (
semanticTree *analyzer.Tree,
path string,
skim bool,
) (
uuid.UUID,
error,
) {
this.Debugln("entering module analysis", path)
// parse module metadata file
var metaTree metaParser.Tree
metaPath := filepath.Join(path, "fspl.mod")
metaFile, err := openAbsolute(this.FS, metaPath)
if err != nil { return uuid.UUID { }, err }
defer metaFile.Close()
lx, err := lexer.LexReader(metaPath, metaFile)
if err != nil { return uuid.UUID { }, err }
err = metaTree.Parse(lx)
if err != nil { return uuid.UUID { }, err }
// ensure metadata is well formed
dependencies := make(map[string] *entity.Dependency)
for _, dependency := range metaTree.Dependencies {
this.Debugln(dependency)
nickname := dependency.Nickname
if nickname == "" {
newNickname, ok := dependency.Address.Nickname()
if !ok {
return uuid.UUID { }, ferrors.Errorf (
dependency.Position,
"cannot generate nickname for %v, " +
"please add one after the address",
dependency.Address)
}
nickname = newNickname
}
if previous, exists := dependencies[nickname]; exists {
return uuid.UUID { }, ferrors.Errorf (
dependency.Position,
"unit with nickname %v already listed at %v",
nickname, previous.Position)
}
dependencies[nickname] = dependency
}
// analyze dependency units, building a nickname translation table
nicknames := make(map[string] uuid.UUID)
dependencyKeys := sortMapKeys(dependencies)
for _, nickname := range dependencyKeys {
dependency := dependencies[nickname]
resolved, err := this.Resolve(path, dependency.Address)
if err != nil { return uuid.UUID { }, err }
dependencyUUID, err := this.AnalyzeUnit(semanticTree, resolved, true)
if err != nil { return uuid.UUID { }, err }
nicknames[nickname] = dependencyUUID
}
// parse this unit
var syntaxTree fsplParser.Tree
err = this.ParseUnit(&syntaxTree, path, skim)
if err != nil { return uuid.UUID { }, err}
// analyze this unit
this.Debugln("analyzing", path, metaTree.UUID)
err = semanticTree.Analyze(metaTree.UUID, nicknames, syntaxTree)
if err != nil { return uuid.UUID { }, err}
return metaTree.UUID, nil
}
func (this *Compiler) AnalyzeSourceFile (
semanticTree *analyzer.Tree,
path string,
skim bool,
) (
uuid.UUID,
error,
) {
this.Debugln("entering source file analysis", path)
// parse this unit
var syntaxTree fsplParser.Tree
err := this.ParseUnit(&syntaxTree, path, skim)
if err != nil { return uuid.UUID { }, err}
// analyze this unit
unitId := entity.Address(path).UUID()
this.Debugln("analyzing", path, unitId)
err = semanticTree.Analyze(unitId, nil, syntaxTree)
if err != nil { return uuid.UUID { }, err}
return unitId, nil
}
func (this *Compiler) ParseUnit (
syntaxTree *fsplParser.Tree,
path string,
skim bool,
) (
error,
) {
filePath, isFile := entity.Address(path).SourceFile()
modulePath, isModule := entity.Address(path).Module()
if !isFile && !isModule {
return errors.New(fmt.Sprintf (
"%v is not a module, nor a source file",
path))
}
if isModule {
return this.ParseModule(syntaxTree, modulePath, skim)
} else {
return this.ParseSourceFile(syntaxTree, filePath, skim)
}
}
func (this *Compiler) ParseModule (
syntaxTree *fsplParser.Tree,
path string,
skim bool,
) (
error,
) {
this.Debugln("parsing module", path)
// parse all files in the module
file, err := openAbsolute(this.FS, path)
if err != nil { return err }
defer file.Close()
dir, ok := file.(fs.ReadDirFile)
if !ok { return errors.New(fmt.Sprintf("%s is not a directory", path)) }
entries, err := dir.ReadDir(0)
if err != nil { return err }
for _, entry := range entries {
if filepath.Ext(entry.Name()) != ".fspl" { continue }
filePath := filepath.Join(path, entry.Name())
file, err := openAbsolute(this.FS, filePath)
if err != nil { return err }
defer file.Close()
lx, err := lexer.LexReader(filePath, file)
if err != nil { return err }
if skim {
err = syntaxTree.Skim(lx)
} else {
err = syntaxTree.Parse(lx)
}
if err != nil { return err }
}
return nil
}
func (this *Compiler) ParseSourceFile (
syntaxTree *fsplParser.Tree,
path string,
skim bool,
) (
error,
) {
this.Debugln("parsing source file", path)
file, err := openAbsolute(this.FS, path)
if err != nil { return err }
defer file.Close()
lx, err := lexer.LexReader(path, file)
if err != nil { return err }
if skim {
err = syntaxTree.Skim(lx)
} else {
err = syntaxTree.Parse(lx)
}
if err != nil { return err }
return nil
}
func sortMapKeys[T any] (unsorted map[string] T) []string {
keys := make([]string, len(unsorted))
index := 0
for key := range unsorted {
keys[index] = key
index ++
}
sort.Strings(keys)
return keys
}

66
compiler/compiler_test.go Normal file
View File

@ -0,0 +1,66 @@
package compiler
import "testing"
func TestHelloWorld (test *testing.T) {
testUnit (test,
"/test-data/data/hello", nil,
"", "Hello, world!\n",
0,
)}
func TestExitCode13 (test *testing.T) {
testUnit (test,
"/test-data/data/exitcode13", nil,
"", "",
13,
)}
func TestSystemInclude (test *testing.T) {
testUnit (test,
"/test-data/data/systeminclude", nil,
"", "Hello, /usr/include!\n",
0,
)}
func TestSystemSrc (test *testing.T) {
dependencies := []string {
compileDependency(test, "io"),
}
testUnit (test,
"/test-data/data/systemsrc", dependencies,
"", "Hello, /usr/src!\n",
0,
)}
func TestArgC (test *testing.T) {
testUnit (test,
"/test-data/data/argc", nil,
"", "",
2, "arg1", "arg2",
)}
// TODO: uncomment once #47 has been dealt with
// func TestArgV (test *testing.T) {
// testUnit (test,
// "/test-data/data/argv", nil,
// "", "This is an argument\n",
// 0, "This is an argument",
// )}
func TestSimpleInterface (test *testing.T) {
testUnit (test,
"/test-data/data/simpleinterface", nil,
"", "",
9,
)}
func TestWriterInterface (test *testing.T) {
dependencies := []string {
compileDependency(test, "io"),
}
testUnit (test,
"/test-data/data/writer", dependencies,
"", "well hello their\n",
0,
)}

3
compiler/doc.go Normal file
View File

@ -0,0 +1,3 @@
// Package compiler is responsible for orchestrating the different FSPL
// compilation stages, as well as invoking the LLVM IR compiler.
package compiler

114
compiler/resolver.go Normal file
View File

@ -0,0 +1,114 @@
package compiler
import "os"
import "fmt"
import "io/fs"
import "errors"
import "strings"
import "path/filepath"
import "git.tebibyte.media/fspl/fspl/entity"
// Resolver turns addresses into absolute filepaths.
type Resolver struct {
// FS specifies a filesystem to search.
FS fs.FS
// Path specifies a list of paths that a unit may exist directly in. The
// Resolver will search the FS for each path listed, starting at the
// first and ending at the last. Thus, paths nearer the start will have
// a higher priority.
Path []string
}
// NewResolver creates a new resolver with os.DirFS("/").
func NewResolver (path ...string) Resolver {
return Resolver {
FS: os.DirFS("/"),
Path: path,
}
}
// AddPath adds one or more items to the resolver's search path.
func (resolver *Resolver) AddPath (path ...string) {
resolver.Path = append(resolver.Path, path...)
}
// AddPathFront adds one or more items to the beginning of the resolver's search
// path.
func (resolver *Resolver) AddPathFront (path ...string) {
// i know how memory works in go babyyyyyy
newPath := make([]string, len(path))
copy(newPath, path)
resolver.Path = append(newPath, resolver.Path...)
}
// Resolve resolves an address into an absolute filepath starting at the
// filesystem root. It follows these rules:
// - If the address starts with '.', '..', it is joined with context
// - If the address starts with '/', it is treated as an absolute path from
// the fs root
// - Else, the address is searched for in the resolver's paths
func (resolver Resolver) Resolve (context string, address entity.Address) (string, error) {
strAddr := string(address)
switch {
case strings.HasPrefix(strAddr, "."):
return filepath.Join(context, strAddr), nil
case strings.HasPrefix(strAddr, "/"):
return strAddr, nil
default:
if resolver.Path == nil || resolver.FS == nil {
return "", errors.New(fmt.Sprintf (
"could not find unit %v: %v",
address, "no search path specified"))
}
location, err := resolver.search(strAddr)
if err != nil {
return "", errors.New(fmt.Sprintf (
"could not find unit %v: %v",
address, err))
}
return location, nil
}
}
func (resolver Resolver) search (needle string) (string, error) {
for _, dirPath := range resolver.Path {
// attempt to open the file as dir.
// if we can't open the dir, just skip it, because it is
// perfectly reasonable that the user might not have
// /usr/local/include/fspl etc.
file, err := openAbsolute(resolver.FS, dirPath)
if err != nil { continue }
dir, ok := file.(fs.ReadDirFile)
if !ok { continue }
entries, err := dir.ReadDir(0)
if err != nil { continue }
dir.Close()
// search through the entries
for _, entry := range entries {
if entry.Name() == needle {
return filepath.Join(dirPath, entry.Name()), nil
}
}
}
return "", fs.ErrNotExist
}
// ResolveCwd resolves the address within the context of the current working
// directory.
func (resolver Resolver) ResolveCwd (address entity.Address) (string, error) {
wd, err := os.Getwd()
if err != nil { return "", err }
return resolver.Resolve(wd, address)
}
// openAbsolute exists because fs.FS implementations do not understand absolute
// paths, which the FSPL compiler runs on. It converts an absolute path to a
// path relative to "/" and opens the file.
func openAbsolute (filesystem fs.FS, path string) (fs.File, error) {
path, err := filepath.Rel("/", path)
if err != nil { return nil, err }
return filesystem.Open(path)
}

View File

@ -0,0 +1 @@
'2b4a9e61-1f87-4173-b6d5-c0174b0abe43'

View File

@ -0,0 +1 @@
[main argc:I32 argv:**Byte]: I32 'main' = [- argc 1]

View File

@ -0,0 +1,2 @@
'ad9c62ae-c580-4c53-86e1-d5bf3ce251e5'
+ 'cstdio'

View File

@ -0,0 +1,5 @@
[main argc:I32 argv:**Byte]: I32 'main' = {
argv = [~~ **Byte [+ [~~Index argv] 1]]
cstdio::[puts [.argv]]
0
}

View File

@ -0,0 +1 @@
'2380d7e1-00a4-44a8-ae4a-30285a158788'

View File

@ -0,0 +1 @@
[main]: Int 'main' = 13

View File

@ -0,0 +1 @@
'241467f1-219a-4f13-8cdf-32ef2ad88277'

View File

@ -0,0 +1,6 @@
[puts string:*Byte]: Index 'puts'
[main]: I32 'main' = {
[puts 'Hello, world!']
0
}

View File

@ -0,0 +1 @@
'1cabcb3c-ebac-4bc1-8610-76cbc47fde3a'

View File

@ -0,0 +1,9 @@
Numbered: (~ [number]: Int)
Number: Int
Number.[number]: Int = [.this]
[main]: I32 'main' = {
num:Number = 9
ifa:Numbered = num
[~I32 ifa.[number]]
}

View File

@ -0,0 +1,2 @@
'15059679-90a0-468a-be57-62f8b958d46b'
+ 'cstdio'

View File

@ -0,0 +1,4 @@
[main]: I32 'main' = {
cstdio::[puts 'Hello, /usr/include!']
0
}

View File

@ -0,0 +1,2 @@
'fb8bddc7-2db1-45f6-b81c-d58a28298ab0'
+ 'io'

View File

@ -0,0 +1,4 @@
[main]: I32 'main' = {
io::[println 'Hello, /usr/src!']
0
}

View File

@ -0,0 +1,2 @@
'7e7ee20f-30fc-441a-943c-2acdb40c9df1'
+ 'io'

View File

@ -0,0 +1,7 @@
[sayHello writer:io::Writer] = writer.[write 'well hello their\n']
[main]: I32 'main' = {
stdout:io::File = 1
[sayHello stdout]
0
}

View File

@ -0,0 +1 @@
'f95aaa14-612c-45cd-b3ae-fd24049cc81b'

View File

@ -0,0 +1,4 @@
+ FileDescriptor: Int
+ [write file:FileDescriptor buffer:*Byte count:Index]: Index 'write'
+ [read file:FileDescriptor buffer:*Byte count:Index]: Index 'read'
+ [puts string:*Byte]: Index 'puts'

View File

@ -0,0 +1,2 @@
'431b60dd-c990-4086-ae30-aad6246b207d'
+ 'cstdio'

View File

@ -0,0 +1,9 @@
+ [print string:String]: Index = cstdio::[write 1 [~*Byte string] [#string]]
+ [println string:String]: Index = [+ [print string] cstdio::[write 1 '\n' 1]]
+ Writer: (~ [write buffer:*:Byte]: Index)
+ File: cstdio::FileDescriptor
+ File.[write buffer:*:Byte]:Index =
cstdio::[write
[~cstdio::FileDescriptor [.this]]
[~*Byte buffer] [#buffer]]

View File

@ -2,7 +2,7 @@
## Top level
### Type definition
Type definitions bind a type to a global identifier.
Type definitions bind a type to a global type identifier.
### Function
Functions bind a global identifier and argument list to an expression which is
evaluated each time the function is called. If no expression is specified, the
@ -17,7 +17,7 @@ the type they are defined on.
## Types
### Named
Named refers to a user-defined or built in named type.
Named refers to a user-defined, primitive, or built-in named type.
### Pointer
Pointer is a pointer to another type.
### Array
@ -38,6 +38,35 @@ Interfaces are always passed by reference. When assigning a value to an
interface, it will be referenced automatically. When assigning a pointer to an
interface, the pointer's reference will be used instead.
## Primitive types
### Int
Int is defined as a signed system word.
### UInt
UInt is defined as an unsigned system word.
### I8, I16, I32, I64
I8-I64 are defined as signed 8, 16, 32, and 64 bit integers respectively.
### U8, U16, U32, U64
U8-U64 are defined as unsigned 8, 16, 32, and 64 bit integers respectively.
### F32, F64
F32 and F64 are defined as single-precision and double-precision floating point
types respectively.
## Built-in types
### Index
Index is defined as an unsigned system word. It is used to describe the size of
chunks of memory, and to index arrays and such.
### Byte
Byte is defined as the smallest addressable integer. It is unsigned. It is
usually equivalent to U8.
### Bool
Bool is a boolean type. It is equivalent to Byte.
### Rune
Rune is defined as a U32. It represents a single UTF-32 code point.
### String
String is defined as a slice of U8's. It represents a UTF-8 string. It is not
conventionally null-terminated, but a null can be added at the end manually if
desired.
## Expressions
### Location expressions
Location expressions are special expressions that only refer to the location of
@ -47,7 +76,8 @@ with a star (*).
### Literals
#### Integer
An integer literal specifies an integer value. It can be assigned to any type
that is derived from an integer, or a float.
that is derived from an integer or a float, as long as the value of the literal
can fit within the range of the type.
It cannot be directly assigned to an interface because it contains no inherent
type information. A value cast may be used for this purpose.
#### Float
@ -55,6 +85,22 @@ A float literal specifies a floating point value. It can be assigned to any type
that is derived from a float.
It cannot be directly assigned to an interface because it contains no inherent
type information. A value cast may be used for this purpose.
#### String
A string literal specifies a string value. It takes on different data
representations depending on what the base type of what it is assigned to is
structurally equivalent to:
- Integer: Single unicode code point. When assigning to an integer, the
string literal may not be longer than one code point, and that code point
must fit in the integer.
- Slice of 8 bit integers: UTF-8 string.
- Slice of 16 bit integers: UTF-16 string.
- Slice of 32 bit (or larger) integers: UTF-32 string.
- Array of integers: The same as slices of integers, but the string literal
must fit inside of the array.
- Pointer to 8 bit integer: Null-terminated UTF-8 string (AKA C-string).
A string literal cannot be directly assigned to an interface because it
contains no inherent type information. A value cast may be used for this
purpose.
#### Array
Array is a composite array literal. It can contain any number of values. It can
be assigned to any array type that:
@ -73,6 +119,11 @@ pairs. It can be assigned to any struct type that:
It cannot be directly assigned to an interface because it contains no inherent
type information. A value cast may be used for this purpose.
#### Boolean
Boolean is a boolean literal. It may be either true or false. It can be assigned
to any type derived from a boolean. It cannot be directly assigned to an
interface because it contains no inherent type information. A value cast may be
used for this purpose.
### Variable *
Variable specifies a named variable. It can be assigned to a type matching the
variable declaration's type. Since it contains inherent type information, it may
@ -89,19 +140,22 @@ value, and any assignment rules of the block are equivalent to those of its last
expression.
### Call
Call calls upon the function specified by the first argument, and passes the
rest of that argument to the function. The first argument must be a function
type, usually the name of a function. The result of a call may be assigned to
rest of that argument to the function. The first argument must be an identifier
referring to usually the name of a function. The result of a call may be assigned to
any type matching the function's return type. Since it contains inherent type
information, it may be directly assigned to an interface.
### Method call
Method call calls upon the method (of the expression before the dot) that is
specified by the first argument, passing the rest of the arguments to the
method. The first argument must be a method name. The result of a call may be
assigned to any type matching the method's return type. Since it contains
inherent type information, it may be directly assigned to an interface.
### Member access *
Member access allows referring to a specific member of a value with a struct
type. It accepts any struct type that contains the specified member name, and
may be assigned to any type that matches the type of the selected member. Since
it contains inherent type information, it may be directly assigned to an
interface.
### Method access
Method access allows referring to a specific method of a type, or a behavior of
an interface. It can only be assigned to the first argument of a call.
### Array subscript *
Array subscripting allows referring to a specific element of an array. It
accepts any array, and any offset of type Size. It may be assigned to any type
@ -112,6 +166,9 @@ Slice adjusts the start and end points of a slice relative to its current
starting index, and returns an adjusted copy pointing to the same data. Any
assignment rules of this expression are equivalent to those of the slice it is
operating on.
### Length
Length returns the length of an array or a slice. It always returns a value
of type Index.
### Pointer dereference *
Pointer dereferencing allows retrieving the value of a pointer. It accepts any
pointer. It may be assigned to any type matching the pointer's pointed type.
@ -201,16 +258,21 @@ does not return anything, the return statement does not accept a value. In all
cases, return statements have no value and may not be assigned to anything.
### Assignment
Assignment allows assigning the result of one expression to one or more location
expressions. The assignment statement itself has no value and may not be
expressions. The assignment expression itself has no value and may not be
assigned to anything.
# Syntax entities
Below is a rough syntax description of the language. Note that `<assignment>`
is right-associative, and `<memberAccess>` and `<methodCall>` are
left-associative. I invite you to torture yourself by attempting to implement
this without hand-writing a parser.
```
<file> -> (<typedef> | <function> | <method>)*
<typedef> -> <identifier> ":" <type>
<typedef> -> <typeIdentifier> ":" <type>
<function> -> <signature> ["=" <expression>]
<method> -> <identifier> "." <function>
<method> -> <typeIdentifier> "." <function>
<type> -> <namedType>
| <pointerType>
@ -218,21 +280,24 @@ assigned to anything.
| <arrayType>
| <structType>
| <interfaceType>
<namedType> -> <identifier>
<namedType> -> <typeIdentifier>
<pointerType> -> "*" <type>
<sliceType> -> "*" ":" <type>
<arrayType> -> <intLiteral> ":" <type>
<structType> -> "(" <declaration>* ")"
<interfaceType> -> "(" <signature> ")"
<structType> -> "(" "." <declaration>* ")"
<interfaceType> -> "(" "~" <signature>* ")"
<expression> -> <intLiteral>
| <floatLiteral>
| <stringLiteral>
| <arrayLiteral>
| <structLiteral>
| <booleanLiteral>
| <variable>
| <declaration>
| <call>
| <subscript>
| <length>
| <dereference>
| <reference>
| <valueCast>
@ -240,24 +305,26 @@ assigned to anything.
| <operation>
| <block>
| <memberAccess>
| <methodCall>
| <ifelse>
| <loop>
| <break>
| <return>
<statement> -> <expression> | <assignment>
| <assignment>
<variable> -> <identifier>
<declaration> -> <identifier> ":" <type>
<call> -> "[" <expression>+ "]"
<subscript> -> "[" "." <expression> <expression> "]"
<slice> -> "[" "\" <expression> <expression>? ":" <expression>? "]"
<slice> -> "[" "\" <expression> <expression>? "/" <expression>? "]"
<length> -> "[" "#" <expression> "]"
<dereference> -> "[" "." <expression> "]"
<reference> -> "[" "@" <expression> "]"
<valueCast> -> "[" "~" <type> <expression> "]"
<bitCast> -> "[" "~~" <type> <expression> "]"
<operation> -> "[" <operator> <expression>* "]"
<block> -> "{" <statement>* "}"
<memberAccess> -> <variable> "." <identifier>
<methodAccess> -> <variable> "::" <identifier>
<block> -> "{" <expression>* "}"
<memberAccess> -> <expression> "." <identifier>
<methodCall> -> <expression> "." <call>
<ifelse> -> "if" <expression>
"then" <expression>
["else" <expression>]
@ -270,16 +337,18 @@ assigned to anything.
| /-?0[0-7]*/
| /-?0x[0-9a-fA-F]*/
| /-?0b[0-1]*/
<floatLiteral> -> /-?[0-9]*\.[0-9]+/
<arrayLiteral> -> "(*" <expression>* ")"
<structLiteral> -> "(" <member>* ")"
<floatLiteral> -> /-?[0-9]*\.[0-9]+/
<stringLiteral> -> /'.*'/
<arrayLiteral> -> "(*" <expression>* ")"
<structLiteral> -> "(." <member>* ")"
<booleanLiteral> -> "true" | "false"
<member> -> <identifier> ":" <expression>
<signature> -> "[" <identifier> <declaration>* "]" [":" <type>]
<identifier> -> /[A-Za-z]+/
<operator> -> "+" | "++" | "-" | "--" | "*" | "/" | "%"
| "!!" | "||" | "&&" | "^^"
| "!" | "|" | "&" | "^" | "<<" | ">>"
| "<" | ">" | "<=" | ">=" | "="
<member> -> <identifier> ":" <expression>
<signature> -> "[" <identifier> <declaration>* "]" [":" <type>]
<identifier> -> /[a-z][A-Za-z]*/
<typeIdentifier> -> /[A-Z][A-Za-z]*/
<operator> -> "+" | "++" | "-" | "--" | "*" | "/" | "%"
| "!!" | "||" | "&&" | "^^"
| "!" | "|" | "&" | "^" | "<<" | ">>"
| "<" | ">" | "<=" | ">=" | "="
```

183
design/units.md Normal file
View File

@ -0,0 +1,183 @@
# Units
## Modules - Concept
- Equivalent to a package in Go
- Contains one or more FSPL source files
- Uniqued by a UUIDv4
- Depends on zero or more other units
- Source files in a module can access functionality of dependencies
## Addressing
When compiling source files, depending on a module, etc. an *address* is used to
refer to a *unit*, which is a module or file. An *addresser* is anything that
addresses a unit, the *addressee*. An addresser can be a module, a user invoking
the compiler, or something else. An address is represented by a string. If the
string ends in `.fspl`, the address refers to an FSPL source file. If not, the
address refers to a module.
If the address begins in a `/`, `./` or `../`, the address is interpreted as an
absolute path beginning from the filesystem root, the current directory of the
addressee, or the directory above that respectively. Otherwise, the unit is
searched for within a set of standard or configured paths.
For example, if the search path is `/usr/include/fspl`, and the address is
`foo`, then the unit will be located at `/usr/include/fspl/foo`. If the address
is `foo/bar`, then the unit will be located at `/usr/include/fspl/foo/bar`. If
there is an additional directory in the search path, such as
`/usr/local/include/fspl`, then the unit will be searched for in each one (in
order) until it is found.
There are standard paths that the compiler will search for units in. These are,
in order of preference:
- `$HOME/.local/src/fspl`
- `$HOME/.local/include/fspl`
- `/usr/local/src/fspl`
- `/usr/local/include/fspl`
- `/usr/src/fspl`
- `/usr/include/fspl`
Files in `include` directories should *not* include program code, and should
only define types and external functions and methods, similar to header files in
C. They may have a corresponding shared object file that programs can
dynamically link against.
Files in `src` directories *may* contain program code, and may be compiled into
an object file if the user wishes to link them statically. Because of FSPL's
ability to "skim" units (discussed later in this document), files in `src` may
be used in the same way that files in `include` are. Thus, `src` files are
effectively more "complete" versions of `include` files with extended
capability, and that is why they are searched first.
## Uniqueness
Each unit is uniqued by a UUID. Most users will never directly use UUIDs, but
they are essential in order to prevent name collisions within the compiler or
linker. For modules, the UUID is specified in the metadata file. For other
units, the UUID is a UUIDv3 (md5) generated using the zero-UUID as a namespace
and the basename of the file (with the extension) as the data.
When creating a module, a UUID should be randomly generated for it. Keep in mind
that altering the UUID of a library will cause programs that used to dynamically
load it to no longer function until they are re-compiled. Therefore, UUIDs
should only be altered if you are introducing breaking ABI changes to your
library. If you are forking an existing module and making changes to it, a
similar rule applies: only keep the same UUID if you intend on keeping an
entirely backwards compatible ABI.
Built-in entities that can be accessed globally from any module (such as the
`String` type) are given a zero-UUID, which represents the "global" unit.
Anything that is a part of this unit is accesisble from any other unit, without
having to use a nickname to refer to it.
When generating code, top-level entities must be named like this if their link
name was not specified manually:
`<uuid>::<name>`
Where `<uuid>` is the base64 encoding of the UUID. For example, the built-in
String type would be assigned the following link name:
`AAAAAAAAAAAAAAAAAAAAAA==::String`
And a type `Bird` in a lone source file with the name `bird.fspl` would be:
`eT/CnSopFDlFwpDCnSEAThjDsBw=::Bird`
Methods are named as follows:
`<uuid>::<name>.<method>`
Where `<uuid>` and `<name>` correspond to the base64 UUID of the unit and the
name of the method's owner type respectively, and `<method>` corresponds to the
method name.
## Module Structure
Each module is represented by a directory, which contains source files along
with a metadata file called `fspl.mod`. The metadata file is of the form:
```
<file> -> <UUIDv4> <directive>*
<directive> -> <depedency>
<dependency> -> "+" <stringLiteral> [<ident>]
<UUIDv4> -> <stringLiteral>
```
Metadata files only make use of tokens defined in the FSPL lexer, and are
designed to make use of the same parsing and lexing infrastructure used to parse
and tokenize source files. A sample metadata file might look like:
```
'5a8353f8-cad8-4604-be60-29a2575996bc'
+ 'io'
+ '../io' customIo
```
The UUID is represented as a string, and so are addresses. When depending on a
unit, it may be "nicknamed" by supplying an identifier after the address. This
changes how the unit is referred to within the module.
## Referencing Units
Compiled by itself, an FSPL source file has no access to other units. However,
when compiling a module as a whole, all source files within the module have
access to units depended on by the module's metadata file. Note that no actual
data or code is imported into the module from the units it depends on, because
all methods and functions defined within them are automatically turned into
prototypes. The module must be linked either statically or dynamically to the
unit's object code after compilation. This is why the FSPL compiler outputs
object files by default.
FSPL source files may reference functions or types from dependencies by
prefixing them with a unit name and a double colon (`::`), like this:
```
reader: io::Reader = x
data: *:Byte = io::[readAll reader]
```
The name of a unit depends on the associated dependency directive used in the
module metadata file. If a nickname is listed, then that is used as the unit
name. Otherwise, the unit name is the basename of the address, which is
normalized and formatted into a valid identifier by the the following rules:
- If the name contains at least one dot, the last dot and everything after it
are removed
- All non-alphabetical and non-numeric characters are removed, and any
alphabetical characters that were directly after them are converted to
uppercase
- All numeric digits at the start of the string are removed
- The first character is converted to lowercase
For example:
- `100-bottles-of-glue_test`
- `Picture.jpg`
- `Just a straight up sentence`
Would become:
- `bottlesOfGlueTest`
- `picture`
- `justAStraightUpSentence`
If the unit name is still not a valid identifier or is empty, the compiler will
refuse to process the module and it is up to the user to either nickname the
unit, or change the unit's basename to something workable.
The compiler will also refuse to process the module if one or more units end up
with the same unit name. However, a function or a variable may have the same
name as a unit because units are only ever used within the context of their own
special syntax (`::`).
## Future Work
Addresses do not necessarily have to refer to units. They could also refer to
arbitrary blobs to embed into a compiled program, similarly to how Go's embed
system works. There of course would need to be a distinction between depending
on units and embedding data, because someone might want to embed an FSPL source
file. Thus, there would need to be a separate metadata file directive, possibly
starting with an `!` or something like that.

4
entity/doc.go Normal file
View File

@ -0,0 +1,4 @@
// Package entity provides data representations of language concepts.
// They are used to construct syntax trees, and semantic trees. It additionally
// provides some utility functions to compare certain entities.
package entity

View File

@ -1,17 +1,14 @@
package entity
import "fmt"
import "github.com/alecthomas/participle/v2/lexer"
import "github.com/google/uuid"
import "git.tebibyte.media/fspl/fspl/errors"
// Expression is any construct that can be evaluated.
type Expression interface {
Type () Type
HasExplicitType () bool
expression ()
Statement
}
// Statement is any construct that can be placed inside of a block expression.
type Statement interface {
statement()
}
// Variable specifies a named variable. It can be assigned to a type matching
@ -19,11 +16,16 @@ type Statement interface {
// it may be directly assigned to an interface. A variable is always a valid
// location expression.
type Variable struct {
Pos lexer.Position
Name string `parser:" @Ident "`
// Syntax
Position errors.Position
Name string
// Semantics
Declaration *Declaration
}
func (*Variable) expression(){}
func (*Variable) statement(){}
func (this *Variable) Type () Type { return this.Declaration.Type() }
func (this *Variable) HasExplicitType () bool { return true }
func (this *Variable) String () string {
return this.Name
}
@ -34,51 +36,103 @@ func (this *Variable) String () string {
// assigned to an interface. A declaration is always a valid location
// expression.
type Declaration struct {
Pos lexer.Position
Name string `parser:" @Ident "`
Type Type `parser:" ':' @@ "`
Position errors.Position
Name string
Ty Type
}
func (*Declaration) expression(){}
func (*Declaration) statement(){}
func (this *Declaration) Type () Type { return this.Ty }
func (this *Declaration) HasExplicitType () bool { return true }
func (this *Declaration) String () string {
return fmt.Sprint(this.Name, ":", this.Type)
return fmt.Sprint(this.Name, ":", this.Ty)
}
// Call calls upon the function specified by the first argument, and passes the
// rest of that argument to the function. The first argument must be a function
// type, usually the name of a function. The result of a call may be assigned to
// any type matching the function's return type. Since it contains inherent type
// information, it may be directly assigned to an interface. A call is never a
// valid location expression.
// Call calls upon the function specified by the first argument, passing the
// rest of the arguments to that function. The first argument must be a function
// name. The result of a call may be assigned to any type matching the
// function's return type. Since it contains inherent type information, it may
// be directly assigned to an interface. A call is never a valid location
// expression.
type Call struct {
Pos lexer.Position
Function Expression `parser:" '[' @@ "`
Arguments []Expression `parser:" @@* ']' "`
// Syntax
Position errors.Position
UnitNickname string
Name string
Arguments []Expression
// Semantics
Function *Function
Unit uuid.UUID
}
func (*Call) expression(){}
func (*Call) statement(){}
func (this *Call) Type () Type { return this.Function.Signature.Return }
func (this *Call) HasExplicitType () bool { return true }
func (this *Call) String () string {
out := fmt.Sprint("[", this.Function)
out := ""
if this.UnitNickname != "" {
out += fmt.Sprint(this.UnitNickname, "::")
}
out += fmt.Sprint("[", this.Name)
for _, argument := range this.Arguments {
out += fmt.Sprint(" ", argument)
}
return out + "]"
}
// Array subscripting allows referring to a specific element of an array. It
// accepts any array, and any offset of type Size. It may be assigned to any
// type matching the array's element type. Since it contains inherent type
// MethodCall calls upon the method of the variable before the dot that is
// specified by the first argument, passing the rest of the arguments to the
// method. The first argument must be a method name. The result of a call may be
// assigned to any type matching the method's return type. Since it contains
// inherent type information, it may be directly assigned to an interface.
// A method call is never a valid location expression.
type MethodCall struct {
// Syntax
Position errors.Position
Source Expression
Name string
Arguments []Expression
// Semantics
Method *Method
Behavior *Signature
Ty Type
}
func (*MethodCall) expression(){}
func (this *MethodCall) Type () Type {
if this.Method != nil {
return this.Method.Signature.Return
} else {
return this.Behavior.Return
}
}
func (this *MethodCall) HasExplicitType () bool { return true }
func (this *MethodCall) String () string {
out := fmt.Sprint(this.Source, ".[", this.Name)
for _, argument := range this.Arguments {
out += fmt.Sprint(" ", argument)
}
return out + "]"
}
// Slice subscripting allows referring to a specific element of a slice. It
// accepts any slice, and any offset of type Size. It may be assigned to any
// type matching the slice's element type. Since it contains inherent type
// information, it may be directly assigned to an interface. A subscript is a
// valid location expression only if the array being subscripted is.
type Subscript struct {
Pos lexer.Position
Array Expression `parser:" '[' '.' @@ "`
Offset Expression `parser:" @@ ']' "`
// Syntax
Position errors.Position
Slice Expression
Offset Expression
// Semantics
Ty Type
}
func (*Subscript) expression(){}
func (*Subscript) statement(){}
func (this *Subscript) Type () Type { return this.Ty }
func (this *Subscript) HasExplicitType () bool { return true }
func (this *Subscript) String () string {
return fmt.Sprint("[.", this.Array, " ", this.Offset, "]")
return fmt.Sprint("[.", this.Slice, " ", this.Offset, "]")
}
// Slice adjusts the start and end points of a slice relative to its current
@ -86,36 +140,60 @@ func (this *Subscript) String () string {
// assignment rules of this expression are equivalent to those of the slice it
// is operating on. A slice is never a valid location expression.
type Slice struct {
Pos lexer.Position
Slice Expression `parser:" '[' '\\\\' @@ "`
Start Expression `parser:" @@? "`
End Expression `parser:" ':' @@? ']' "`
// Syntax
Position errors.Position
Slice Expression
Start Expression
End Expression
}
func (*Slice) expression(){}
func (*Slice) statement(){}
func (this *Slice) Type () Type { return this.Slice.Type() }
func (this *Slice) HasExplicitType () bool { return true }
func (this *Slice) String () string {
out := fmt.Sprint("[\\", this.Slice, " ")
if this.Start != nil {
out += fmt.Sprint(this.Start)
}
out += ":"
out += "/" // TODO have trailing decimals pop off of numbers, replace this with ..
if this.End != nil {
out += fmt.Sprint(this.End)
}
return out + "]"
}
// Length returns the length of an array or a slice. It always returns a value
// of type Index. A length is never a valid location expression.
type Length struct {
// Syntax
Position errors.Position
Slice Expression
// Semantics
Ty Type
}
func (*Length) expression(){}
func (this *Length) Type () Type { return this.Ty }
func (this *Length) HasExplicitType () bool { return true }
func (this *Length) String () string {
return fmt.Sprint("[#", this.Slice, "]")
}
// Pointer dereferencing allows retrieving the value of a pointer. It accepts
// any pointer. It may be assigned to any type matching the pointer's pointed
// type. Since it contains inherent type information, it may be directly
// assigned to an interface. A dereference is a valid location expression only
// if the pointer being dereferenced is.
type Dereference struct {
Pos lexer.Position
Pointer Expression `parser:" '[' '.' @@ ']' "`
// Syntax
Position errors.Position
Pointer Expression
// Semantics
Ty Type
}
func (*Dereference) expression(){}
func (*Dereference) statement(){}
func (this *Dereference) Type () Type { return this.Ty }
func (this *Dereference) HasExplicitType () bool { return true }
func (this *Dereference) String () string {
return fmt.Sprint("[.", this.Pointer, "]")
}
@ -128,11 +206,16 @@ func (this *Dereference) String () string {
// automatically references it anyway. A reference is never a valid location
// expression.
type Reference struct {
Pos lexer.Position
Value Expression `parser:" '[' '@' @@ ']' "`
// Syntax
Position errors.Position
Value Expression
// Semantics
Ty Type
}
func (*Reference) expression(){}
func (*Reference) statement(){}
func (this *Reference) Type () Type { return this.Ty }
func (this *Reference) HasExplicitType () bool { return true }
func (this *Reference) String () string {
return fmt.Sprint("[@", this.Value, "]")
}
@ -141,14 +224,15 @@ func (this *Reference) String () string {
// contains inherent type information, it may be directly assigned to an
// interface. A value cast is never a valid location expression.
type ValueCast struct {
Pos lexer.Position
Type Type `parser:" '[' '~' @@ "`
Value Expression `parser:" @@ ']' "`
Position errors.Position
Ty Type
Value Expression
}
func (*ValueCast) expression(){}
func (*ValueCast) statement(){}
func (this *ValueCast) Type () Type { return this.Ty }
func (this *ValueCast) HasExplicitType () bool { return true }
func (this *ValueCast) String () string {
return fmt.Sprint("[~ ", this.Type, this.Value, "]")
return fmt.Sprint("[~ ", this.Ty, this.Value, "]")
}
// Bit casting takes the raw data in memory of a certain value and re-interprets
@ -156,14 +240,15 @@ func (this *ValueCast) String () string {
// it may be directly assigned to an interface. A bit cast is never a valid
// location expression.
type BitCast struct {
Pos lexer.Position
Type Type `parser:" '[' '~''~' @@ "`
Value Expression `parser:" @@ ']' "`
Position errors.Position
Ty Type
Value Expression
}
func (*BitCast) expression(){}
func (*BitCast) statement(){}
func (this *BitCast) Type () Type { return this.Ty }
func (this *BitCast) HasExplicitType () bool { return true }
func (this *BitCast) String () string {
return fmt.Sprint("[~~ ", this.Type, this.Value, "]")
return fmt.Sprint("[~~ ", this.Ty, this.Value, "]")
}
// Operations perform math, logic, or bit manipulation on values. They accept
@ -171,13 +256,19 @@ func (this *BitCast) String () string {
// special cases. Since they contain no inherent type information, they may not
// be assigned to interfaces. An operation is never a valid location expression.
type Operation struct {
Pos lexer.Position
// FIXME super janky, need to make a custom lexer at some point
Operator Operator `parser:" '[' @('+''+' | '+' | '-''-' | '-' | '*' | '/' | '%' | '!''!' | '|''|' | '&''&' | '^''^' | '!' | '|' | '&' | '^' | '<''<' | '>''>' | '<' | '>' | '<''=' | '>''=' | '=') "`
Arguments []Expression `parser:" @@+ ']' "`
// Syntax
Position errors.Position
Operator Operator
Arguments []Expression
// Semantics
Ty Type
}
func (*Operation) expression(){}
func (*Operation) statement(){}
func (this *Operation) Type () Type { return this.Ty }
func (this *Operation) HasExplicitType () bool {
return this.Operator.ResultsInBoolean()
}
func (this *Operation) String () string {
out := fmt.Sprint("[", this.Operator)
for _, argument := range this.Arguments {
@ -193,14 +284,19 @@ func (this *Operation) String () string {
// expression.
type Block struct {
// Syntax
Pos lexer.Position
Steps []Statement `parser:" '{' @@* '}' "`
Position errors.Position
Steps []Expression
// Semantics
Ty Type
Scope
}
func (*Block) expression(){}
func (*Block) statement(){}
func (this *Block) Type () Type { return this.Ty }
func (this *Block) HasExplicitType () bool {
if len(this.Steps) == 0 { return false }
return this.Steps[len(this.Steps) - 1].HasExplicitType()
}
func (this *Block) String () string {
out := "{"
for index, step := range this.Steps {
@ -217,43 +313,41 @@ func (this *Block) String () string {
// an interface. A member access is a valid location expression only when the
// struct being accessed is.
type MemberAccess struct {
Pos lexer.Position
Source *Variable `parser:" @@ "`
Member string `parser:" '.' @Ident "`
// Syntax
Position errors.Position
Source Expression
Member string
// Semantics
Ty Type
}
func (*MemberAccess) expression(){}
func (*MemberAccess) statement(){}
func (this *MemberAccess) Type () Type { return this.Ty }
func (this *MemberAccess) HasExplicitType () bool { return true }
func (this *MemberAccess) String () string {
return fmt.Sprint(this.Source, ".", this.Member)
}
// Method access allows referring to a specific method of a type, or a behavior
// of an interface. It can only be assigned to the first argument of a call. A
// method access is never a valid location expression.
type MethodAccess struct {
Pos lexer.Position
Source *Variable `parser:" @@ "`
Member string `parser:" ':' ':' @Ident "`
}
func (*MethodAccess) expression(){}
func (*MethodAccess) statement(){}
func (this *MethodAccess) String () string {
return fmt.Sprint(this.Source, "::", this.Member)
}
// If/else is a control flow branching expression that executes one of two
// expressions depending on a boolean value. If the value of the if/else is
// unused, the else expression need not be specified. It may be assigned to any
// type that satisfies the assignment rules of both the true and false
// expressions. An If/else is never a valid location expression.
type IfElse struct {
Pos lexer.Position
Condition Expression `parser:" 'if' @@ "`
True Expression `parser:" 'then' @@ "`
False Expression `parser:" ('else' @@)? "`
// Syntax
Position errors.Position
Condition Expression
True Expression
False Expression
// Semantics
Ty Type
}
func (*IfElse) expression(){}
func (*IfElse) statement(){}
func (this *IfElse) Type () Type { return this.Ty }
func (this *IfElse) HasExplicitType () bool {
return this.True.HasExplicitType()
}
func (this *IfElse) String () string {
out := fmt.Sprint("if ", this.Condition, " then ", this.True)
if this.False != nil {
@ -270,11 +364,20 @@ func (this *IfElse) String () string {
// break statements only apply to the closest containing loop. The value of the
// loop's expression is never used. A loop is never a valid location expression.
type Loop struct {
Pos lexer.Position
Body Expression `parser:" 'loop' @@ "`
// Syntax
Position errors.Position
Body Expression
// Semantics
Ty Type
}
func (*Loop) expression(){}
func (*Loop) statement(){}
func (this *Loop) Type () Type { return this.Ty }
func (this *Loop) HasExplicitType () bool {
// this is as accurate as possible without doing a full analysis of the
// loop
return false
}
func (this *Loop) String () string {
return fmt.Sprint("loop ", this.Body)
}
@ -282,11 +385,16 @@ func (this *Loop) String () string {
// Break allows breaking out of loops. It has no value and may not be assigned
// to anything. It is never a valid location expression.
type Break struct {
Pos lexer.Position
Value Expression `parser:" '[' 'break' @@? ']' "`
// Syntax
Position errors.Position
Value Expression
// Semantics
Loop *Loop
}
func (*Break) expression(){}
func (*Break) statement(){}
func (this *Break) Type () Type { return nil }
func (this *Break) HasExplicitType () bool { return false }
func (this *Break) String () string {
if this.Value == nil {
return "[break]"
@ -300,11 +408,16 @@ func (this *Break) String () string {
// value. In all cases, return statements have no value and may not be assigned
// to anything. A return statement is never a valid location expression.
type Return struct {
Pos lexer.Position
Value Expression `parser:" '[' 'return' @@? ']' "`
// Syntax
Position errors.Position
Value Expression
// Semantics
Declaration TopLevel
}
func (*Return) expression(){}
func (*Return) statement(){}
func (this *Return) Type () Type { return nil }
func (this *Return) HasExplicitType () bool { return false }
func (this *Return) String () string {
if this.Value == nil {
return "[return]"
@ -318,11 +431,13 @@ func (this *Return) String () string {
// not be assigned to anything. An assignment statement is never a valid
// location expression.
type Assignment struct {
Pos lexer.Position
Location Expression `parser:" @@ "`
Value Expression `parser:" '=' @@ "`
Position errors.Position
Location Expression
Value Expression
}
func (*Assignment) statement(){}
func (*Assignment) expression(){}
func (this *Assignment) Type () Type { return nil }
func (this *Assignment) HasExplicitType () bool { return false }
func (this *Assignment) String () string {
return fmt.Sprint(this.Location, "=", this.Value)
}

View File

@ -1,18 +1,24 @@
package entity
import "fmt"
import "github.com/alecthomas/participle/v2/lexer"
import "git.tebibyte.media/fspl/fspl/errors"
// LiteralInt specifies an integer value. It can be assigned to any type that is
// derived from an integer, or a float. It cannot be directly assigned to an
// derived from an integer or a float, as long as the value fo the literal can
// fit within the range of the type. It cannot be directly assigned to an
// interface because it contains no inherent type information. A value cast may
// be used for this purpose.
type LiteralInt struct {
Pos lexer.Position
Value int `parser:" @Int "`
// Syntax
Position errors.Position
Value int
// Semantics
Ty Type
}
func (*LiteralInt) expression(){}
func (*LiteralInt) statement(){}
func (this *LiteralInt) Type () Type { return this.Ty }
func (this *LiteralInt) HasExplicitType () bool { return false }
func (this *LiteralInt) String () string {
return fmt.Sprint(this.Value)
}
@ -22,28 +28,70 @@ func (this *LiteralInt) String () string {
// because it contains no inherent type information. A value cast may be used
// for this purpose.
type LiteralFloat struct {
Pos lexer.Position
Value float64 `parser:" @Float "`
// Syntax
Position errors.Position
Value float64
// Semantics
Ty Type
}
func (*LiteralFloat) expression(){}
func (*LiteralFloat) statement(){}
func (this *LiteralFloat) Type () Type { return this.Ty }
func (this *LiteralFloat) HasExplicitType () bool { return false }
func (this *LiteralFloat) String () string {
return fmt.Sprint(this.Value)
}
// Array is a composite array literal. It can contain any number of values. It
// can be assigned to any array type that:
// LiteralString specifies a string value. It takes on different data
// representations depending on what the base type of what it is assigned to is
// structurally equivalent to:
// - Integer: Single unicode code point. When assigning to an integer, the
// string literal may not be longer than one code point, and that code point
// must fit in the integer.
// - Slice of 8 bit integers: UTF-8 string.
// - Slice of 16 bit integers: UTF-16 string.
// - Slice of 32 bit (or larger) integers: UTF-32 string.
// - Array of integers: The same as slices of integers, but the string literal
// must fit inside of the array.
// - Pointer to 8 bit integer: Null-terminated UTF-8 string (AKA C-string).
// A string literal cannot be directly assigned to an interface because it
// contains no inherent type information. A value cast may be used for this
// purpose.
type LiteralString struct {
// Syntax
Position errors.Position
ValueUTF8 string
ValueUTF16 []uint16
ValueUTF32 []rune
// Semantics
Ty Type
}
func (*LiteralString) expression(){}
func (this *LiteralString) Type () Type { return this.Ty }
func (this *LiteralString) HasExplicitType () bool { return false }
func (this *LiteralString) String () string {
return Quote(this.ValueUTF8)
}
// LiteralArray is a composite array literal. It can contain any number of
// values. It can be assigned to any array type that:
// 1. has an identical length, and
// 2. who's element type can be assigned to by all the element values in the
// literal.
// It cannot be directly assigned to an interface because it contains no
// inherent type information. A value cast may be used for this purpose.
type LiteralArray struct {
Pos lexer.Position
Elements []Expression `parser:" '(' '*' @@* ')' "`
// Syntax
Position errors.Position
Elements []Expression
// Semantics
Ty Type
}
func (*LiteralArray) expression(){}
func (*LiteralArray) statement(){}
func (this *LiteralArray) Type () Type { return this.Ty }
func (this *LiteralArray) HasExplicitType () bool { return false }
func (this *LiteralArray) String () string {
out := "(*"
for _, element := range this.Elements {
@ -52,7 +100,7 @@ func (this *LiteralArray) String () string {
return out + ")"
}
// Struct is a composite structure literal. It can contain any number of
// LiteralStruct is a composite structure literal. It can contain any number of
// name:value pairs. It can be assigned to any struct type that:
// 1. has at least the members specified in the literal
// 2. who's member types can be assigned to by the corresponding member
@ -60,16 +108,57 @@ func (this *LiteralArray) String () string {
// It cannot be directly assigned to an interface because it contains no
// inherent type information. A value cast may be used for this purpose.
type LiteralStruct struct {
Pos lexer.Position
Members []Member `parser:" '(' @@* ')' "`
// Syntax
Position errors.Position
Members []*Member
// Semantics
Ty Type
MemberOrder []string
MemberMap map[string] *Member
}
func (*LiteralStruct) expression(){}
func (*LiteralStruct) statement(){}
func (this *LiteralStruct) Type () Type { return this.Ty }
func (this *LiteralStruct) HasExplicitType () bool { return false }
func (this *LiteralStruct) String () string {
out := "("
for index, member := range this.Members {
if index > 1 { out += " " }
out += fmt.Sprint(member)
out := "(."
for _, member := range this.Members {
out += fmt.Sprint(" ", member)
}
return out + ")"
}
// LiteralBoolean is a boolean literal. It may be either true or false. It can
// be assigned to any type derived from a boolean. It cannot be directly
// assigned to an interface because it contains no inherent type information. A
// value cast may be used for this purpose.
type LiteralBoolean struct {
// Syntax
Position errors.Position
Value bool
// Semantics
Ty Type
}
func (*LiteralBoolean) expression(){}
func (this *LiteralBoolean) Type () Type { return this.Ty }
func (this *LiteralBoolean) HasExplicitType () bool { return false }
func (this *LiteralBoolean) String () string {
if this.Value {
return "true"
} else {
return "false"
}
}
type LiteralNil struct {
// Syntax
Position errors.Position
// Semantics
Ty Type
}
func (*LiteralNil) expression(){}
func (this *LiteralNil) Type () Type { return this.Ty }
func (this *LiteralNil) HasExplicitType () bool { return false }
func (this *LiteralNil) String () string { return "nil" }

120
entity/meta.go Normal file
View File

@ -0,0 +1,120 @@
package entity
import "fmt"
import "strings"
import "unicode"
import "unicode/utf8"
import "path/filepath"
import "github.com/google/uuid"
import "git.tebibyte.media/fspl/fspl/errors"
// Metadata represents a module metadata file.
type Metadata struct {
Position errors.Position
UUID uuid.UUID
Dependencies []*Dependency
}
func (this *Metadata) String () string {
out := fmt.Sprint(Quote(this.UUID.String()))
for _, dependency := range this.Dependencies {
out += fmt.Sprint("\n", dependency)
}
return out
}
// Directive is a declaration within a module metadata file.
type Directive interface {
directive()
}
// Dependency is a metadata dependency listing.
type Dependency struct {
Position errors.Position
Address Address
Nickname string
}
func (*Dependency) directive () { }
func (this *Dependency) String () string {
out := fmt.Sprint("+ ", this.Address)
if this.Nickname != "" {
out += fmt.Sprint(" ", this.Nickname)
}
return out
}
// Address is the address of a unit.
type Address string
func (addr Address) String () string {
return Quote(string(addr))
}
// SourceFile returns the FSPL source file associated with the address. If the
// address does not point to an FSPL source file, it returns "", false.
func (addr Address) SourceFile () (string, bool) {
ext := filepath.Ext(string(addr))
if ext == ".fspl" {
return filepath.Clean(string(addr)), true
} else {
return "", false
}
}
// Module returns the module associated with the address. If the address does
// does not point to a module, it returns "", false.
func (addr Address) Module () (string, bool) {
path := string(addr)
switch {
case filepath.Base(path) == "fspl.mod":
return filepath.Dir(path), true
case filepath.Ext(path) == "" || filepath.Ext(path) == ".":
return filepath.Clean(path), true
default:
return "", false
}
}
// Nickname automatically generates a nickname from the address, which is a
// valid Ident token. On failure "", false is returned. The nickname is
// generated according to the folowing rules:
//
// - If the name contains at least one dot, the last dot and everything after
// it is removed
// - All non-letter, non-numeric characters are removed, and any letters that
// were directly after them are converted to uppercase
// - All numeric digits at the start of the string are removed
// - The first character is converted to lowercase
func (addr Address) Nickname () (string, bool) {
// get the normalized basename with no extension
base := filepath.Base(string(addr))
base = strings.TrimSuffix(base, filepath.Ext(base))
// remove all non-letter, non-digit characters and convert to camel case
nickname := ""
uppercase := false
for _, char := range base {
if unicode.IsLetter(char) || unicode.IsDigit(char) {
if uppercase {
uppercase = false
char = unicode.ToUpper(char)
}
nickname += string(char)
} else {
uppercase = true
}
}
// remove numeric digits
for len(nickname) > 0 {
char, size := utf8.DecodeRuneInString(nickname)
nickname = nickname[size:]
if unicode.IsLetter(char) {
// lowercase the first letter
nickname = string(unicode.ToLower(char)) + nickname
break
}
}
return nickname, nickname != ""
}
// UUID automatically generates a UUID from the address. It is the MD5 hash of
// the basename, with a space of the zero UUID.
func (addr Address) UUID () uuid.UUID {
return uuid.NewMD5(uuid.UUID { }, []byte(filepath.Base(string(addr))))
}

View File

@ -1,19 +1,29 @@
package entity
import "fmt"
import "strings"
import "github.com/alecthomas/participle/v2/lexer"
import "unicode"
import "github.com/google/uuid"
import "git.tebibyte.media/fspl/fspl/errors"
// Signature is a function or method signature that is used in functions,
// methods, and specifying interface behaviors. It defines the type of a
// function.
type Signature struct {
Pos lexer.Position
Name string `parser:" '[' @Ident "`
Arguments []*Declaration `parser:" @@* ']' "`
Return Type `parser:" ( ':' @@ )? "`
// Syntax
Position errors.Position
Name string
Arguments []*Declaration
Return Type
// Semantics
Acc Access
Unt uuid.UUID
ArgumentOrder []string
ArgumentMap map[string] *Declaration
}
func (*Signature) ty(){}
func (this *Signature) Access () Access { return this.Acc }
func (this *Signature) Unit () uuid.UUID { return this.Unt }
func (this *Signature) String () string {
out := "[" + this.Name
for _, argument := range this.Arguments {
@ -25,13 +35,27 @@ func (this *Signature) String () string {
}
return out
}
func (this *Signature) Equals (ty Type) bool {
real, ok := ty.(*Signature)
if !ok ||
len(real.Arguments) != len(this.Arguments) ||
!TypesEqual(real.Return, this.Return) ||
real.Name != this.Name { return false }
for index, argument := range this.Arguments {
if !argument.Type().Equals(real.Arguments[index].Type()) {
return false
}
}
return true
}
// Member is a syntactical construct that is used to list members in struct
// literals.
type Member struct {
Pos lexer.Position
Name string `parser:" @Ident "`
Value Expression `parser:" ':' @@ "`
Position errors.Position
Name string
Value Expression
}
func (this *Member) String () string {
return fmt.Sprint(this.Name, ":", this.Value)
@ -71,6 +95,25 @@ type Operator int; const (
OperatorEqual
)
// ResultsInBoolean determines whether or not this operation will always result
// in a boolean value.
func (operator Operator) ResultsInBoolean () bool {
switch operator {
case OperatorLogicalNot,
OperatorLogicalOr,
OperatorLogicalAnd,
OperatorLogicalXor,
OperatorLess,
OperatorGreater,
OperatorLessEqual,
OperatorGreaterEqual,
OperatorEqual:
return true
default:
return false
}
}
var operatorToString = []string {
// Math
"+",
@ -86,6 +129,7 @@ var operatorToString = []string {
"|",
"&",
"^",
// Bit manipulation
"!!",
"||",
@ -111,11 +155,25 @@ func init () {
}
}
func (this *Operator) Capture (str []string) error {
*this = stringToOperator[strings.Join(str, "")]
return nil
func OperatorFromString (str string) Operator {
return stringToOperator[str]
}
func (this Operator) String () string {
return operatorToString[this]
}
// Quote puts quotes around a string to turn it into a string literal.
func Quote (in string) string {
out := "'"
for _, char := range in {
if char == '\'' {
out += "\\'"
} else if unicode.IsPrint(char) {
out += string(char)
} else {
out += fmt.Sprintf("\\%03o", char)
}
}
return out + "'"
}

View File

@ -8,7 +8,11 @@ type Scoped interface {
Variable (name string) *Declaration
// AddVariable adds a variable to this entity's scope.
AddVariable (name string, declaration *Declaration)
AddVariable (declaration *Declaration)
// OverVariables calls callback for each defined variable, stopping if
// returns false.
OverVariables (callback func (*Declaration) bool)
}
// Scope implements a scope.
@ -23,9 +27,15 @@ func (this *Scope) Variable (name string) *Declaration {
return this.variables[name]
}
func (this *Scope) AddVariable (name string, declaration *Declaration) {
func (this *Scope) AddVariable (declaration *Declaration) {
if this.variables == nil {
this.variables = make(map[string] *Declaration)
}
this.variables[name] = declaration
this.variables[declaration.Name] = declaration
}
func (this *Scope) OverVariables (callback func (*Declaration) bool) {
for _, declaration := range this.variables {
if !callback(declaration) { break }
}
}

View File

@ -1,33 +1,100 @@
package entity
import "fmt"
import "github.com/alecthomas/participle/v2/lexer"
import "encoding/base64"
import "github.com/google/uuid"
import "git.tebibyte.media/fspl/fspl/errors"
// Key globally indexes top level entities in contexts where modules matter.
type Key struct {
Unit uuid.UUID
Name string
Method string
}
func (key Key) String () string {
out := fmt.Sprintf("%v::%v", key.Unit, key.Name)
if key.Method != "" {
out = fmt.Sprintf("%s.%s", out, key.Method)
}
return out
}
// LinkName returns the name that the entity it refers to will be given when
// compiled.
func (key Key) LinkName () string {
data := [16]byte(key.Unit)
out := fmt.Sprintf(
"%s::%s",
base64.StdEncoding.EncodeToString(data[:]),
key.Name)
if key.Method != "" {
out = fmt.Sprintf("%s.%s", out, key.Method)
}
return out
}
// StripMethod returns a copy of the key that refers to a type instead of a
// method.
func (key Key) StripMethod () Key {
key.Method = ""
return key
}
// TopLevel is any construct that is placed at the root of a file.
type TopLevel interface {
topLevel ()
}
// Access determines the external access control mode for a top-level entity.
type Access int; const (
// AccessPublic allows other modules to access an entity normally.
AccessPublic Access = iota
// AccessRestricted causes a top-level entity to appear opaque to other
// modules. Values of restricted types can be passed around, assigned
// to eachother, and their methods can be called, but the implementation
// of the type is entirely hidden. This access mode cannot be applied to
// functions.
AccessRestricted
// AccessPrivate disallows other modules from accessing a top-level
// entity.
AccessPrivate
)
func (this Access) String () string {
switch this {
case AccessPrivate: return "-"
case AccessRestricted: return "~"
case AccessPublic: return "+"
default: return fmt.Sprintf("entity.Access(%d)", this)
}
}
// Typedef binds a type to a global identifier.
type Typedef struct {
// Syntax
Pos lexer.Position
Public bool `parser:" @'+'? "`
Name string `parser:" @Ident "`
Type Type `parser:" ':' @@ "`
Position errors.Position
Acc Access
Name string
Type Type
// Semantics
Methods map[string] Method
Unit uuid.UUID
Methods map[string] *Method
}
func (*Typedef) topLevel(){}
func (this *Typedef) String () string {
out := fmt.Sprint(this.Name, ": ", this.Type)
output := ""
output += fmt.Sprint(this.Acc, " ")
output += fmt.Sprint(this.Name, ": ", this.Type)
if this.Methods != nil {
for _, method := range this.Methods {
out += fmt.Sprint("\n", method)
output += fmt.Sprint("\n", method)
}
}
return out
return output
}
// Function binds a global identifier and argument list to an expression which
@ -37,46 +104,59 @@ func (this *Typedef) String () string {
// these are typed.
type Function struct {
// Syntax
Pos lexer.Position
Public bool `parser:" @'+'? "`
Signature *Signature `parser:" @@ "`
Body Expression `parser:" ( '=' @@ )? "`
Position errors.Position
Acc Access
LinkName string
Signature *Signature
Body Expression
// Semantics
Unit uuid.UUID
Scope
}
func (*Function) topLevel(){}
func (this *Function) String () string {
if this.Body == nil {
return fmt.Sprint(this.Signature)
} else {
return fmt.Sprint(this.Signature, " = ", this.Body)
output := ""
output += fmt.Sprint(this.Acc, " ")
output += this.Signature.String()
if this.LinkName != "" {
output += fmt.Sprint(" '", this.LinkName, "'")
}
if this.Body != nil {
output += fmt.Sprint(" = ", this.Body)
}
return output
}
// Method is like a function, except localized to a defined type. Methods are
// called on an instance of that type, and receive a pointer to that instance
// via the "this" variable. Method names are not globally unique, bur are unique
// within the type they are defined on.
// via the "this" variable when they are run. Method names are not globally
// unique, but are unique within the type they are defined on.
type Method struct {
// Syntax
Pos lexer.Position
Public bool `parser:" @'+'? "`
TypeName string `parser:" @Ident "`
Signature *Signature `parser:" ':' ':' @@ "`
Body Expression `parser:" ( '=' @@ )? "`
Position errors.Position
Acc Access
TypeName string
LinkName string
Signature *Signature
Body Expression
// Semantics
Unit uuid.UUID
Type Type
This *Declaration
Scope
}
func (*Method) topLevel(){}
func (this *Method) String () string {
if this.Body == nil {
return fmt.Sprint(this.TypeName, ".", this.Signature)
} else {
return fmt.Sprint (
this.TypeName, "::",
this.Signature, " = ",
this.Body)
output := ""
output += fmt.Sprint(this.Acc, " ")
output += fmt.Sprint(this.TypeName, ".", this.Signature)
if this.LinkName != "" {
output += fmt.Sprint(" '", this.LinkName, "'")
}
if this.Body != nil {
output += fmt.Sprint(" = ", this.Body)
}
return output
}

View File

@ -1,108 +1,278 @@
package entity
import "fmt"
import "github.com/alecthomas/participle/v2/lexer"
import "github.com/google/uuid"
import "git.tebibyte.media/fspl/fspl/errors"
// Type is any type notation.
type Type interface {
fmt.Stringer
// Equals reports whether this type is equivalent to another type.
Equals (ty Type) bool
// Access reports the access permission of the type.
Access () Access
// Unit reports the unit that the type was defined in.
Unit () uuid.UUID
ty ()
}
type void { }
func (void) ty(){}
// Void returns the absence of a type. It can be compared to a Type to find out
// if it is filled in, but has no type.
func Void () Type {
return void { }
}
// TypeNamed refers to a user-defined or built in named type.
type TypeNamed struct {
Pos lexer.Position
Name string `parser:" @Ident "`
Type Type
// Syntax
Position errors.Position
UnitNickname string
Name string
Type Type
// Semantics
Acc Access
Unt uuid.UUID
}
func (*TypeNamed) ty(){}
func (this *TypeNamed) String () string { return this.Name }
func (this *TypeNamed) Access () Access { return this.Acc }
func (this *TypeNamed) Unit () uuid.UUID { return this.Unt }
func (this *TypeNamed) String () string {
if this.UnitNickname == "" {
return this.Name
} else {
return fmt.Sprint(this.UnitNickname, "::", this.Name)
}
}
func (this *TypeNamed) Equals (ty Type) bool {
real, ok := ty.(*TypeNamed)
return ok && TypesEqual(real.Type, this.Type)
}
// TypePointer is a pointer to another type.
type TypePointer struct {
Pos lexer.Position
Referenced Type `parser:" '*' @@ "`
Position errors.Position
Referenced Type
// Semantics
Acc Access
Unt uuid.UUID
}
func (*TypePointer) ty(){}
func (this *TypePointer) Access () Access { return this.Acc }
func (this *TypePointer) Unit () uuid.UUID { return this.Unt }
func (this *TypePointer) String () string {
return fmt.Sprint("*", this.Referenced)
}
func (this *TypePointer) Equals (ty Type) bool {
real, ok := ty.(*TypePointer)
return ok && TypesEqual(real.Referenced, this.Referenced)
}
// TypeSlice is a pointer to several values of a given type stored next to
// eachother. Its length is not built into its type and can be changed at
// runtime.
type TypeSlice struct {
Pos lexer.Position
Element Type `parser:" '*' ':' @@ "`
Position errors.Position
Element Type
// Semantics
Acc Access
Unt uuid.UUID
}
func (*TypeSlice) ty(){}
func (this *TypeSlice) Access () Access { return this.Acc }
func (this *TypeSlice) Unit () uuid.UUID { return this.Unt }
func (this *TypeSlice) String () string {
return fmt.Sprint("*:", this.Element)
}
func (this *TypeSlice) Equals (ty Type) bool {
real, ok := ty.(*TypeSlice)
return ok && TypesEqual(real.Element, this.Element)
}
// TypeArray is a group of values of a given type stored next to eachother. The
// length of an array is fixed and is part of its type. Arrays are passed by
// value unless a pointer is used.
type TypeArray struct {
Pos lexer.Position
Length int `parser:" @Int "`
Element Type `parser:" ':' @@ "`
Position errors.Position
Length int
Element Type
// Semantics
Acc Access
Unt uuid.UUID
}
func (*TypeArray) ty(){}
func (this *TypeArray) Access () Access { return this.Acc }
func (this *TypeArray) Unit () uuid.UUID { return this.Unt }
func (this *TypeArray) String () string {
return fmt.Sprint(this.Length, ":", this.Element)
}
func (this *TypeArray) Equals (ty Type) bool {
real, ok := ty.(*TypeArray)
return ok &&
real.Length == this.Length &&
TypesEqual(real.Element, this.Element)
}
// Struct is a composite type that stores keyed values. The positions of the
// TypeStruct is a composite type that stores keyed values. The positions of the
// values within the struct are decided at compile time, based on the order they
// are specified in. Structs are passed by value unless a pointer is used.
type TypeStruct struct {
// Syntax
Pos lexer.Position
Members []*Declaration `parser:" '(' @@+ ')' "`
Position errors.Position
Members []*Declaration
// Semantics
Acc Access
Unt uuid.UUID
MemberOrder []string
MemberMap map[string] *Declaration
MemberMap map[string] *Declaration
}
func (*TypeStruct) ty(){}
func (this *TypeStruct) Access () Access { return this.Acc }
func (this *TypeStruct) Unit () uuid.UUID { return this.Unt }
func (this *TypeStruct) String () string {
out := "("
for index, member := range this.Members {
if index > 0 { out += " " }
out += fmt.Sprint(member)
out := "(."
for _, member := range this.Members {
out += fmt.Sprint(" ", member)
}
return out + ")"
}
func (this *TypeStruct) Equals (ty Type) bool {
real, ok := ty.(*TypeStruct)
if !ok || len(real.Members) != len(this.Members) { return false }
// Interface is a polymorphic pointer that allows any value of any type through,
// except it must have at least the methods defined within the interface.
// Interfaces are always passed by reference. When assigning a value to an
// interface, it will be referenced automatically. When assigning a pointer to
// an interface, the pointer's reference will be used instead.
for index, member := range this.Members {
if !TypesEqual(member.Type(), real.Members[index].Type()) {
return false
}
}
return true
}
// TypeInterface is a polymorphic pointer that allows any value of any type
// through, except it must have at least the methods defined within the
// interface. Interfaces are always passed by reference. When assigning a value
// to an interface, it will be referenced automatically. When assigning a
// pointer to an interface, the pointer's reference will be used instead.
type TypeInterface struct {
// Syntax
Pos lexer.Position
Behaviors []*Signature `parser:" '(' @@+ ')' "`
Position errors.Position
Behaviors []*Signature
// Semantics
Acc Access
Unt uuid.UUID
BehaviorOrder []string
BehaviorMap map[string] *Signature
BehaviorMap map[string] *Signature
}
func (*TypeInterface) ty(){}
func (this *TypeInterface) Access () Access { return this.Acc }
func (this *TypeInterface) Unit () uuid.UUID { return this.Unt }
func (this *TypeInterface) String () string {
out := "("
for index, behavior := range this.Behaviors {
if index > 0 { out += " " }
out += fmt.Sprint(behavior)
out := "(~"
for _, behavior := range this.Behaviors {
out += fmt.Sprint(" ", behavior)
}
return out + ")"
}
func (this *TypeInterface) Equals (ty Type) bool {
real, ok := ty.(*TypeInterface)
if !ok || len(real.Behaviors) != len(this.Behaviors) { return false }
for index, behavior := range this.Behaviors {
if !behavior.Equals(real.Behaviors[index]) {
return false
}
}
return true
}
// TypeInt represents any signed or unsigned integer type.
type TypeInt struct {
Position errors.Position
Width int
Signed bool
// Semantics
Acc Access
Unt uuid.UUID
}
func (*TypeInt) ty(){}
func (this *TypeInt) Access () Access { return this.Acc }
func (this *TypeInt) Unit () uuid.UUID { return this.Unt }
func (this *TypeInt) String () string {
if this.Signed {
return fmt.Sprint("I", this.Width)
} else {
return fmt.Sprint("U", this.Width)
}
}
func (this *TypeInt) Equals (ty Type) bool {
real, ok := ty.(*TypeInt)
return ok && real.Width == this.Width && real.Signed == this.Signed
}
// TypeFloat represents any floating point type.
type TypeFloat struct {
Position errors.Position
Width int
// Semantics
Acc Access
Unt uuid.UUID
}
func (*TypeFloat) ty(){}
func (this *TypeFloat) Access () Access { return this.Acc }
func (this *TypeFloat) Unit () uuid.UUID { return this.Unt }
func (this *TypeFloat) String () string {
return fmt.Sprint("F", this.Width)
}
func (this *TypeFloat) Equals (ty Type) bool {
real, ok := ty.(*TypeFloat)
return ok && real.Width == this.Width
}
// TypeWord represents an integer type of unspecified width. The optimal width
// is chosen based on the machine word size (32 on 32 bit systems, 64 on 64 bit
// systems, etc)
type TypeWord struct {
Position errors.Position
Signed bool
// Semantics
Acc Access
Unt uuid.UUID
}
func (*TypeWord) ty(){}
func (this *TypeWord) Access () Access { return this.Acc }
func (this *TypeWord) Unit () uuid.UUID { return this.Unt }
func (this *TypeWord) String () string {
if this.Signed {
return "Int"
} else {
return "UInt"
}
}
func (this *TypeWord) Equals (ty Type) bool {
real, ok := ty.(*TypeWord)
return ok && real.Signed == this.Signed
}
// TypesEqual checks if two types are equal to eachother, even if one or both
// are nil.
func TypesEqual (left, right Type) bool {
if (left == nil) != (right == nil) { return false }
if left == nil { return true }
return left.Equals(right)
}
// FormatType returns a string representing a type. If the type is nil, it
// returns "Void".
func FormatType (ty Type) string {
if ty == nil {
return "Void"
} else {
return ty.String()
}
}

5
errors/doc.go Normal file
View File

@ -0,0 +1,5 @@
// Package errors provides a location tracking and error formatting system.
// It provides a way to keep track of the in-file position of the physical text
// that in-memory data structures were derived from, and a way to use this
// positional information to create informative and easy to read errors.
package errors

62
errors/errors.go Normal file
View File

@ -0,0 +1,62 @@
package errors
import "fmt"
// Error is an error that contains positional information.
type Error interface {
error
Position () Position
}
type errInternal struct {
position Position
message string
}
var _ Error = new(errInternal)
func (err *errInternal) Error () string {
return err.String()
}
func (err *errInternal) String () string {
return err.message
}
func (err *errInternal) Position () Position {
return err.position
}
// Errorf produces an error with a fmt'd message.
func Errorf (position Position, format string, variables ...any) Error {
return &errInternal {
position: position,
message: fmt.Sprintf(format, variables...),
}
}
// Format returns a formatted string representing an error. This string may take
// up multiple lines and contain ANSI escape codes. If err does not fulfill the
// Error interface, err.Error() is returned instead.
//
// When the error fulfills the Error interface, the formatted output will
// be of the general form:
// FILE:ROW+1:START+1: MESSAGE
// ROW+1 | LINE
// ^^^^
// Where the position of the underline (^^^^) corresponds to the start and end
// fields in the error's position. Note that the row and column numbers as
// displayed are increased by one from their normal zero-indexed state, because
// most editors display row and column numbers starting at 1:1. Additionally,
// because normal error messages do not produce trailing line breaks, neither
// does this function.
func Format (err error) string {
if e, ok := err.(Error); ok {
return fmt.Sprintf (
"%v: %v\n%v",
e.Position(),
e.Error(),
e.Position().Format())
} else {
return err.Error()
}
}

89
errors/errors_test.go Normal file
View File

@ -0,0 +1,89 @@
package errors
import "testing"
import "strings"
func TestError (test *testing.T) {
testError (test,
`example.fspl:11:7: some error
11 | lorem ipsum dolor
^^^^^`,
Errorf (
Position {
File: "example.fspl",
Line: "lorem ipsum dolor",
Row: 10,
Start: 6,
End: 11,
},
"some error"))
}
func TestErrorTab (test *testing.T) {
testError (test,
`example.fspl:11:8: some error
11 | lorem ipsum dolor
^^^^^`,
Errorf (
Position {
File: "example.fspl",
Line: "\tlorem\tipsum\tdolor",
Row: 10,
Start: 7,
End: 12,
},
"some error"))
}
func TestErrorTabInBetween (test *testing.T) {
testError (test,
`example.fspl:11:8: some error
11 | lorem ipsum dolor
^^^^^^^^^`,
Errorf (
Position {
File: "example.fspl",
Line: "\tlorem\tipsum\tdolor",
Row: 10,
Start: 7,
End: 14,
},
"some error"))
}
func TestGetXInTabbedString (test *testing.T) {
getXCase := func (line string, column, correct int) {
x := getXInTabbedString(line, column)
if x != correct {
test.Logf("[%s]: %d", formatTabs(line), column)
test.Log("expecting", correct, "got", x)
test.Fail()
}
}
getXCase("\t", 1, 8)
getXCase("x\ty", 1, 1)
getXCase("x\ty", 2, 8)
getXCase("x\tyyy", 3, 9)
getXCase("x\ty", 3, 9)
getXCase("x\tyyyy\tzy", 7, 16)
}
func TestFormatTabs (test *testing.T) {
fmtCase := func (line string, correct string) {
got := formatTabs(line)
if got != correct {
test.Logf("[%s]", strings.ReplaceAll(line, "\t", "\\t"))
test.Logf("expecting [%s] got [%s]", correct, got)
test.Fail()
}
}
fmtCase("\t", " ")
fmtCase("\ty", " y")
fmtCase("x\ty", "x y")
fmtCase("x\tyyyy\tz", "x yyyy z")
fmtCase("x\tyyyyyyy\tz", "x yyyyyyy z")
fmtCase("\tjustice!\tz", " justice! z")
}

92
errors/position.go Normal file
View File

@ -0,0 +1,92 @@
package errors
import "fmt"
import "strings"
// Position stores the position of something within a file. For memory's sake,
// positions in the same file should be created using the same File string, and
// positions on the same line should be created using the same Line string.
type Position struct {
File string // The name of the file
Line string // the text of the line the position is on
Row int // Line number, starting at 0
Start int // Starting column number, starting at 0
End int // Terminating column number, starting at 0
}
// String returns a string representation of the position of the form:
// FILE:ROW+1:START+1
// Note that the row and column numbers as displayed are increased by one from
// their normal zero-indexed state, because most editors display row and column
// numbers starting at 1:1.
func (pos Position) String () string {
return fmt.Sprintf("%s:%d:%d", pos.File, pos.Row + 1, pos.Start + 1)
}
// Format produces a formatted printout of where the position is in its file.
func (pos Position) Format () string {
line := formatTabs(pos.Line)
output := fmt.Sprintf("%-4d | %s\n ", pos.Row + 1, line)
start := pos.Start
end := pos.End
if end <= start { end = start + 1 }
start = getXInTabbedString(pos.Line, start)
end = getXInTabbedString(pos.Line, end)
for index := range line {
if index >= end { break }
if index >= start{
output += "^"
} else {
output += " "
}
}
return output
}
func getXInTabbedString (tabbed string, column int) int {
var x int
for index, ch := range tabbed {
if index >= column { return x }
if ch == '\t' {
nextStop := ((x / 8) + 1) * 8
x = nextStop - 1
}
x ++
}
return x
}
func formatTabs (tabbed string) string {
var result string
var x int
for _, ch := range tabbed {
if ch == '\t' {
nextStop := ((x / 8) + 1) * 8
result += strings.Repeat(" ", nextStop - x)
x = nextStop - 1
} else {
result += string(ch)
}
x ++
}
return result
}
// Union attempts to return the union of two positions. Since a position cannot
// span multiple lines or files, the resulting position will be on the same line
// and in the same file as the method reciever.
func (pos Position) Union (other Position) Position {
switch {
case other.File != pos.File:
case other.Row < pos.Row: pos.Start = 0
case other.Row > pos.Row: pos.End = len(pos.Line)
default:
if other.Start < pos.Start { pos.Start = other.Start }
if other.End > pos.End { pos.End = other.End }
}
return pos
}

16
errors/test-common.go Normal file
View File

@ -0,0 +1,16 @@
package errors
import "testing"
import "git.tebibyte.media/fspl/fspl/testcommon"
func testString (test *testing.T, correct string, got string) {
if got != correct {
test.Logf("results do not match")
test.Fail()
}
testcommon.Compare(test, correct, got)
}
func testError (test *testing.T, correct string, err Error) {
testString(test, correct, Format(err))
}

86
generator/README.md Normal file
View File

@ -0,0 +1,86 @@
# generator
## Responsibilities
Given a compilation target, turn a well-formed FSPL semantic tree into an LLVM
IR module tree.
## Organization
Generator defines the Target type, which contains information about the system
that the program is being compiled for. The native sub-package uses Go's
conditional compilation directives to provide a default Target that matches the
system the compiler has been natively built for.
The entry point for all logic defined in this package is Target.Generate(). This
method creates a new generator, and uses it to recursively generate and return an
LLVM module. The details of the generator are hidden from other packages, and
instances of it only last for the duration of Target.Generate().
The generator contains a stack of blockManagers, which plays a similar role to
analyzer.scopeContextManager, except that the stack of blockManagers is managed
directly by the generator, which contains appropriate methods for
pushing/popping them.
Like the analyzer, the generator greedily generates code, and one function may
be generated in the middle of the generation process of another function. Thus,
each blockManager is tied to a specific LLVM function, and is in charge of
variables/stack allocations and to a degree, control flow flattening
(specifically loops). It also embeds the current active block, allowing for
generator routines to call its methods to add new instructions to the current
block, and switch between different blocks when necessary.
## Operation
When Target.Generate() is called, a new generator is created. It is given the
semantic tree to generate, as well as a copy of the Target. All data structure
initialization within the generator happens at this point.
Then, the generate() method on the newly created generator is called. This is
the entry point for the actual generation logic. This routine is comprised of
two phases:
- Function generation
- Method generation
You'll notice that there is no step for type generation. This is because types
are generated on-demand in order to reduce IR clutter.
## Expression Generation
Since expressions make up the bulk of FSPL, expression generation makes up the
bulk of the code generator. The generator is able to produce expressions in one
of three modes:
- Location: The generator will return an IR register that contains a pointer to
the result of the expression.
- Value: The generator will return an IR register that directly contains the
result of the expression.
- Any: The generator will decide which of these two options is best for the
specific expression, and will let the caller know which was chosen, in case it
cares. Some expressions are better suited to returning a pointer, such as
array subscripting or member access. Other expressions are better suited to
returning a value, such as arithmetic operators and function calls.
It is important to note that generating a Value expression may return a pointer,
because *FSPL pointers are first-class values*. The distinction between location
and value generation modes is purely to do with LLVM. It is similar to the
concept of location expressions within the analyzer, but not 100% identical all
of the time.
Whenever an expression needs to be generated, one of the following routines is
called:
- generator.generateExpression()
- generator.generateAny()
- generator.generateVal()
- generator.generateLoc()
The generator.generateExpression() routine takes in a mode value and depending
on it, calls one of the other more specific routines. Each of these routines, in
turn, calls a more specialized generation routine depending on the specific
expression.
If it is specifically requested to generate a value for an expression with only
its location component defined or vice versa, generator.generateVal/Loc() will
automatically perform the conversion.

245
generator/assignment.go Normal file
View File

@ -0,0 +1,245 @@
package generator
import "fmt"
import "errors"
import "git.tebibyte.media/fspl/fspl/llvm"
import "git.tebibyte.media/fspl/fspl/entity"
import "git.tebibyte.media/fspl/fspl/analyzer"
func (this *generator) generateAssignment (assignment *entity.Assignment) (llvm.Value, error) {
destination, err := this.generateExpressionLoc(assignment.Location)
if err != nil { return nil, err }
return this.generateAssignmentToDestination (
assignment.Value,
assignment.Location.Type(),
destination)
}
// TODO possibly break generateAssignmentToDestination into several routines,
// each handling one destination type
// generateAssignmentToDestination performs type coercions if necessary, mainly
// interface assignment. This should be called when the user is assigning a
// value to something via an assignment statement, a function/method call, or a
// field in a composite literal.
// irDestLoc specifies the location to assign the source to. If it is nil, this
// function will allocate a destination (if necessary) and return its value as a
// register.
func (this *generator) generateAssignmentToDestination ( // TODO: add -Val suffix
source entity.Expression,
destType entity.Type,
irDestLoc llvm.Value,
) (
llvm.Value,
error,
) {
destinationSpecified := irDestLoc != nil
destTypeBase := analyzer.ReduceToBase(destType)
switch destTypeBase := destTypeBase.(type) {
// conversion from any type to interface
case *entity.TypeInterface:
// methods defined on interface or pointer types cannot pass
// through an interface!
// create destination interface, if necessary
irDestType, err := this.generateType(destType)
if err != nil { return nil, err }
if !destinationSpecified {
irDestLoc = this.blockManager.newAllocaFront(irDestType)
}
destDataFieldLoc := this.getInterfaceDataFieldLoc(irDestLoc, irDestType)
sourceType := source.Type()
switch sourceTypeBase := analyzer.ReduceToBase(sourceType).(type) {
// conversion from interface to interface
case *entity.TypeInterface:
// re-assign data
source, err := this.generateExpressionLoc(source)
if err != nil { return nil, err }
irFromType, err := this.generateType(sourceType)
if err != nil { return nil, err }
sourceDataFieldLoc := this.getInterfaceDataFieldLoc(source, irFromType)
sourceDataLoc := this.blockManager.NewLoad(new(llvm.TypePointer), sourceDataFieldLoc)
this.blockManager.NewStore(sourceDataLoc, destDataFieldLoc)
// re-assign behaviors
for _, name := range destTypeBase.BehaviorOrder {
fromBehaviorField := this.getInterfaceBehaviorFieldLoc (
sourceTypeBase, source,
irFromType, name)
toBehaviorField := this.getInterfaceBehaviorFieldLoc (
destTypeBase, irDestLoc,
irDestType, name)
fromBehavior := this.blockManager.NewLoad(new(llvm.TypePointer), fromBehaviorField)
this.blockManager.NewStore(fromBehavior, toBehaviorField)
}
// conversion from pointer to interface
case *entity.TypePointer:
// assign data
source, err := this.generateExpressionVal(source)
if err != nil { return nil, err }
sourceDataLoc := this.blockManager.NewLoad(new(llvm.TypePointer), source)
this.blockManager.NewStore(sourceDataLoc, destDataFieldLoc)
sourceType, ok := sourceTypeBase.Referenced.(*entity.TypeNamed)
if !ok {
return nil, errors.New(fmt.Sprint(
"can't assign", sourceTypeBase, "to interface"))
}
// assign behaviors
for _, name := range destTypeBase.BehaviorOrder {
toBehaviorFieldAddress := this.getInterfaceBehaviorFieldLoc (
destTypeBase, irDestLoc,
irDestType, name)
key := entity.Key {
Unit: sourceType.Unit(),
Name: sourceType.Name,
Method: name,
}
fromBehavior, err := this.method(key)
if err != nil { return nil, err }
this.blockManager.NewStore(fromBehavior, toBehaviorFieldAddress)
}
// conversion from other type to interface
default:
// assign data
sourceLoc, err := this.generateExpressionLoc(source)
if err != nil { return nil, err }
this.blockManager.NewStore(sourceLoc, destDataFieldLoc)
sourceType, ok := sourceType.(*entity.TypeNamed)
if !ok {
return nil, errors.New(fmt.Sprint(
"can't assign", sourceType, "to interface"))
}
// assign behaviors
for _, name := range destTypeBase.BehaviorOrder {
toBehaviorFieldAddress := this.getInterfaceBehaviorFieldLoc (
destTypeBase, irDestLoc,
irDestType, name)
key := entity.Key {
Unit: sourceType.Type.Unit(),
Name: sourceType.Name,
Method: name,
}
fromBehavior, err := this.method(key)
if err != nil { return nil, err }
this.blockManager.NewStore(fromBehavior, toBehaviorFieldAddress)
}
}
if destinationSpecified {
return nil, nil
} else {
return this.blockManager.NewLoad(irDestType, irDestLoc), nil
}
// conversion from any type to pointer
case *entity.TypePointer:
// assignments
switch source := source.(type) {
// assignment from array literal to pointer
case *entity.LiteralArray:
if destinationSpecified {
_, err := this.generateLiteralArrayLoc(source, irDestLoc)
return nil, err
}
// assignment from string literal to pointer
case *entity.LiteralString:
if destinationSpecified {
_, err := this.generateLiteralStringLoc(source, irDestLoc)
return nil, err
}
}
// conversion from any type to slice
case *entity.TypeSlice:
// conversions
switch sourceTypeBase := analyzer.ReduceToBase(source.Type()).(type) {
// conversion from array to slice
case *entity.TypeArray:
array, err := this.generateExpressionLoc(source)
if err != nil { return nil, err }
irDestType, err := this.generateType(destType)
if err != nil { return nil, err }
if !destinationSpecified {
irDestLoc = this.blockManager.newAllocaFront(irDestType)
}
destDataField := this.getSliceDataFieldLoc(irDestLoc, irDestType)
destLengthField := this.getSliceLengthFieldLoc(irDestLoc, irDestType)
this.blockManager.NewStore(array, destDataField)
this.blockManager.NewStore (
llvm.NewConstInt(llvm.I32, int64(sourceTypeBase.Length)),
destLengthField)
if !destinationSpecified {
return this.blockManager.NewLoad(irDestType, irDestLoc), nil
}
}
// assignments
switch source := source.(type) {
// assignment from array literal to slice
case *entity.LiteralArray:
if destinationSpecified {
_, err := this.generateLiteralArrayLoc(source, irDestLoc)
return nil, err
}
// assignment from string literal to slice
case *entity.LiteralString:
if destinationSpecified {
_, err := this.generateLiteralStringLoc(source, irDestLoc)
return nil, err
}
}
// conversion from any type to array
case *entity.TypeArray:
// assignments
switch source := source.(type) {
// assignment from array literal to array
case *entity.LiteralArray:
if destinationSpecified {
_, err := this.generateLiteralArrayLoc(source, irDestLoc)
return nil, err
}
// assignment from string literal to array
case *entity.LiteralString:
if destinationSpecified {
_, err := this.generateLiteralStringLoc(source, irDestLoc)
return nil, err
}
}
// conversion from any type to struct
case *entity.TypeStruct:
// assignments
switch source := source.(type) {
// assignment from struct literal to struct
case *entity.LiteralStruct:
if destinationSpecified {
_, err := this.generateLiteralStructLoc(source, irDestLoc)
return nil, err
}
}
}
irSource, err := this.generateExpressionVal(source)
if err != nil { return nil, err }
if destinationSpecified {
this.blockManager.NewStore(irSource, irDestLoc)
return nil, nil
} else {
return irSource, nil
}
}

131
generator/blockmanager.go Normal file
View File

@ -0,0 +1,131 @@
package generator
import "fmt"
import "errors"
import "git.tebibyte.media/fspl/fspl/llvm"
import "git.tebibyte.media/fspl/fspl/entity"
type loopEntry struct {
value llvm.Value
stub *llvm.Block
mode resultMode
loc bool
}
type blockManager struct {
*llvm.Block
generator *generator
function *llvm.Function
declarations map[*entity.Declaration] llvm.Value
loops []*loopEntry
}
func (this *blockManager) String () string {
return fmt.Sprint(this.function.Name())
}
func (this *generator) pushBlockManager (function *llvm.Function) *blockManager {
manager := new(blockManager)
manager.generator = this
manager.function = function
manager.newBlock()
manager.declarations = make(map[*entity.Declaration] llvm.Value)
this.managerStack = append(this.managerStack, manager)
this.blockManager = manager
return manager
}
func (this *generator) popBlockManager () {
this.managerStack = this.managerStack[:len(this.managerStack) - 1]
if len(this.managerStack) > 0 {
this.blockManager = this.managerStack[len(this.managerStack) - 1]
} else {
this.blockManager = nil
}
}
func (this *blockManager) pushLoop (mode resultMode) *loopEntry {
entry := &loopEntry { mode: mode }
this.loops = append(this.loops, entry)
return entry
}
func (this *blockManager) popLoop () {
this.loops = this.loops[:len(this.loops) - 1]
}
func (this *blockManager) topLoop () *loopEntry {
return this.loops[len(this.loops) - 1]
}
func (this *blockManager) newAllocaFront (ty llvm.Type) llvm.Value {
alloca := llvm.NewAlloca(ty)
firstBlock := this.function.Blocks[0]
if firstBlock.Terminated() {
lastIndex := len(firstBlock.Instructions) - 1
firstBlock.Instructions = append (
firstBlock.Instructions,
firstBlock.Instructions[lastIndex])
firstBlock.Instructions[lastIndex] = alloca
} else {
firstBlock.AddInstruction(alloca)
}
return alloca
}
func (this *blockManager) newBlock () *llvm.Block {
this.Block = this.function.NewBlock()
return this.Block
}
func (this *blockManager) variable (declaration *entity.Declaration) (llvm.Value, error) {
value, ok := this.declarations[declaration]
if !ok {
return nil, errors.New(fmt.Sprintf("declaration of %v does not exist", declaration.Name))
}
return value, nil
}
func (this *blockManager) addDeclaration (declaration *entity.Declaration, initial llvm.Value) (llvm.Value, error) {
ty, err := this.generator.generateType(declaration.Type())
if err != nil { return nil, err }
location := this.newAllocaFront(ty)
this.declarations[declaration] = location
if initial != nil {
this.NewStore(initial, location)
}
return location, nil
}
func (this *blockManager) addFunctionArgumentDeclarations (scope entity.Scoped) error {
var err error
switch scope := scope.(type) {
case *entity.Function:
for index, argument := range scope.Signature.Arguments {
if argument.Name == argument.Name {
this.addDeclaration (
argument,
this.function.Parameters[index])
}
}
case *entity.Method:
this.addDeclaration (
scope.This,
this.function.Parameters[0])
for index, argument := range scope.Signature.Arguments {
if argument.Name == argument.Name {
this.addDeclaration (
argument,
this.function.Parameters[index + 1])
}
}
}
return err
}

129
generator/cast_test.go Normal file
View File

@ -0,0 +1,129 @@
package generator
import "testing"
func TestValueCastSlicePointer (test *testing.T) {
testString (test,
`%"AAAAAAAAAAAAAAAAAAAAAA==::Index" = type i64
define void @"0zNZN147MN2wzMAQ6NS2dQ==::main"() {
0:
%1 = alloca { ptr, %"AAAAAAAAAAAAAAAAAAAAAA==::Index" }
%2 = alloca ptr
%3 = getelementptr { ptr, %"AAAAAAAAAAAAAAAAAAAAAA==::Index" }, ptr %1, i32 0, i32 0
%4 = load ptr, ptr %3
store ptr %4, ptr %2
ret void
}
`,
`
[main] = {
slice: *:Byte
pointer: *Byte = [~*Byte slice]
}
`)
}
func TestValueCastIntegerIneffectual (test *testing.T) {
testString (test,
`%"0zNZN147MN2wzMAQ6NS2dQ==::A" = type i64
define void @"0zNZN147MN2wzMAQ6NS2dQ==::main"() {
0:
%1 = alloca i64
store i64 5, ptr %1
%2 = alloca %"0zNZN147MN2wzMAQ6NS2dQ==::A"
%3 = load i64, ptr %1
store i64 %3, ptr %2
ret void
}
`,
`
A: Int
[main] = {
x:Int = 5
y:A = [~A x]
}
`)
}
func TestValueCastStringIneffectual (test *testing.T) {
testString (test,
`%"AAAAAAAAAAAAAAAAAAAAAA==::Index" = type i64
%"AAAAAAAAAAAAAAAAAAAAAA==::String" = type { ptr, %"AAAAAAAAAAAAAAAAAAAAAA==::Index" }
define void @"0zNZN147MN2wzMAQ6NS2dQ==::main"() {
0:
%1 = alloca { ptr, %"AAAAAAAAAAAAAAAAAAAAAA==::Index" }
%2 = alloca %"AAAAAAAAAAAAAAAAAAAAAA==::String"
%3 = load { ptr, %"AAAAAAAAAAAAAAAAAAAAAA==::Index" }, ptr %1
store { ptr, %"AAAAAAAAAAAAAAAAAAAAAA==::Index" } %3, ptr %2
ret void
}
`,
`
[main] = {
y:*:Byte
x:String = [~String y]
}
`)
}
func TestBitCastStringIneffectual (test *testing.T) {
testString (test,
`%"AAAAAAAAAAAAAAAAAAAAAA==::Index" = type i64
%"AAAAAAAAAAAAAAAAAAAAAA==::String" = type { ptr, %"AAAAAAAAAAAAAAAAAAAAAA==::Index" }
define void @"0zNZN147MN2wzMAQ6NS2dQ==::main"() {
0:
%1 = alloca { ptr, %"AAAAAAAAAAAAAAAAAAAAAA==::Index" }
%2 = alloca %"AAAAAAAAAAAAAAAAAAAAAA==::String"
%3 = load { ptr, %"AAAAAAAAAAAAAAAAAAAAAA==::Index" }, ptr %1
store { ptr, %"AAAAAAAAAAAAAAAAAAAAAA==::Index" } %3, ptr %2
ret void
}
`,
`
[main] = {
y:*:Byte
x:String = [~~String y]
}
`)
}
// TODO: Perhaps run all generator output in these tests through llc to check the
// integrity of the IR.
// TODO: uncomment and complete once bitcasting is fully supported
// func TestBitCastPointer (test *testing.T) {
// testString (test,
// `
// `,
// `
// Struct: (. x:Int y:Int)
// Array: 4:Int
// [main] = {
// ptr:*Byte
// stc:Struct
// arr:Array
// str:String
// flt:F64
// idx:Index
//
// ; struct
// ptr = [~~*Byte [~~Struct ptr]]
// stc = [~~Struct [~~*Byte stc]]
// ; array
// ptr = [~~*Byte [~~Array ptr]]
// arr = [~~Array [~~*Byte arr]]
// ; slice
// ptr = [~~*Byte [~~String ptr]]
// str = [~~String [~~*Byte str]]
// ; int
// ptr = [~~*Byte [~~Index ptr]]
// idx = [~~Index [~~*Byte idx]]
// ; float
// ptr = [~~*Byte [~~F64 ptr]]
// flt = [~~F64 [~~*Byte flt]]
//
// ; arithmetic
// ptr = [~~*Byte [+ 1 [~~Index ptr]]]
// }
// `)
// }

View File

@ -0,0 +1,158 @@
package generator
import "testing"
func TestBranchAssign (test *testing.T) {
testString (test,
`define void @"0zNZN147MN2wzMAQ6NS2dQ==::main"() {
0:
%1 = alloca i64
br i1 true, label %2, label %4
2:
store i64 5, ptr %1
br label %3
3:
ret void
4:
store i64 6, ptr %1
br label %3
}
`,
`
[main] = {
x:Int
if true then {
x = 5
} else {
x = 6
}
}
`)
}
func TestBranchAssignNoFalse (test *testing.T) {
testString (test,
`define void @"0zNZN147MN2wzMAQ6NS2dQ==::main"() {
0:
%1 = alloca i64
br i1 true, label %2, label %3
2:
store i64 5, ptr %1
br label %3
3:
ret void
}
`,
`
[main] = {
x:Int
if true then {
x = 5
}
}
`)
}
func TestBranchResult (test *testing.T) {
testString (test,
`define void @"0zNZN147MN2wzMAQ6NS2dQ==::main"() {
0:
%1 = alloca i64
br i1 true, label %2, label %5
2:
br label %3
3:
%4 = phi i64 [ 5, %2 ], [ 6, %5 ]
store i64 %4, ptr %1
ret void
5:
br label %3
}
`,
`
[main] = {
x:Int = if true then 5 else 6
}
`)
}
func TestLoop (test *testing.T) {
testString (test,
`define void @"0zNZN147MN2wzMAQ6NS2dQ==::main"() {
0:
br label %1
1:
br label %1
2:
ret void
}
`,
`
[main] = {
loop {
}
}
`)
}
func TestLoopResult (test *testing.T) {
testString (test,
`define void @"0zNZN147MN2wzMAQ6NS2dQ==::main"() {
0:
%1 = alloca i64
br label %2
2:
br label %3
3:
store i64 5, ptr %1
ret void
}
`,
`
[main] = {
x:Int = loop {
[break 5]
}
}
`)
}
func TestLoopBranchResult (test *testing.T) {
testString (test,
`define i64 @"0zNZN147MN2wzMAQ6NS2dQ==::main"() {
0:
%1 = alloca i64
store i64 6, ptr %1
br label %2
2:
%3 = load i64, ptr %1
%4 = icmp slt i64 %3, 3
br i1 %4, label %5, label %8
5:
%6 = load i64, ptr %1
br label %11
7:
br label %2
8:
%9 = load i64, ptr %1
%10 = sub i64 %9, 1
store i64 %10, ptr %1
br label %7
11:
ret i64 %6
}
`,
`
[main]:Int = {
y:Int = 6
loop {
if [< y 3]
then [break y]
else {
y = [-- y]
}
}
}
`)
}

117
generator/data.go Normal file
View File

@ -0,0 +1,117 @@
package generator
import "git.tebibyte.media/fspl/fspl/llvm"
import "git.tebibyte.media/fspl/fspl/entity"
func (this *generator) generateSlice (ty entity.Type, data llvm.Value, length llvm.Value) (llvm.Value, error) {
irType, err := this.generateType(ty)
if err != nil { return nil, err }
slice := this.blockManager.newAllocaFront(irType)
this.sliceSetData(ty, slice, data)
this.sliceSetLength(ty, slice, length)
return slice, nil
}
func (this *generator) generateSliceDefinedLength (ty entity.Type, data llvm.Value, length int64) (llvm.Value, error) {
indexType, err := this.generateTypeIndex()
if err != nil { return nil, err }
return this.generateSlice(ty, data, llvm.NewConstInt(indexType, length))
}
func (this *generator) sliceSetData (ty entity.Type, slice llvm.Value, data llvm.Value) error {
irType, err := this.generateType(ty)
if err != nil { return err }
dataField := this.blockManager.NewGetElementPtr (
irType, slice,
llvm.NewConstInt(llvm.I32, 0),
llvm.NewConstInt(llvm.I32, 0))
this.blockManager.NewStore(data, dataField)
return nil
}
func (this *generator) sliceSetLength (ty entity.Type, slice llvm.Value, length llvm.Value) error {
irType, err := this.generateType(ty)
if err != nil { return err }
lengthField := this.blockManager.NewGetElementPtr (
irType, slice,
llvm.NewConstInt(llvm.I32, 0),
llvm.NewConstInt(llvm.I32, 1))
this.blockManager.NewStore(length, lengthField)
return nil
}
func (this *generator) sliceSetDefinedLength (ty entity.Type, slice llvm.Value, length int64) error {
indexType, err := this.generateTypeIndex()
if err != nil { return err }
return this.sliceSetLength(ty, slice, llvm.NewConstInt(indexType, length))
}
func (this *generator) getSliceDataAddress (slice llvm.Value, irType llvm.Type) llvm.Value {
return this.blockManager.NewLoad (
new(llvm.TypePointer),
this.getSliceDataFieldLoc(slice, irType))
}
func (this *generator) getSliceDataFieldLoc (slice llvm.Value, irType llvm.Type) llvm.Value {
return this.blockManager.NewGetElementPtr (
irType, slice,
llvm.NewConstInt(llvm.I32, 0),
llvm.NewConstInt(llvm.I32, 0))
}
func (this *generator) getSliceLengthFieldLoc (slice llvm.Value, irType llvm.Type) llvm.Value {
return this.blockManager.NewGetElementPtr (
irType, slice,
llvm.NewConstInt(llvm.I32, 0),
llvm.NewConstInt(llvm.I32, 1))
}
func (this *generator) getInterfaceDataFieldLoc (iface llvm.Value, irType llvm.Type) llvm.Value {
return this.blockManager.NewGetElementPtr (
irType, iface,
llvm.NewConstInt(llvm.I32, 0),
llvm.NewConstInt(llvm.I32, 0))
}
func (this *generator) getInterfaceBehaviorFieldLoc (
ty *entity.TypeInterface,
iface llvm.Value,
irType llvm.Type,
name string,
) llvm.Value {
offset := this.getInterfaceBehaviorIndex(ty, name)
if offset > -1 {
return this.blockManager.NewGetElementPtr (
irType, iface,
llvm.NewConstInt(llvm.I32, 0),
llvm.NewConstInt(llvm.I32, int64(offset + 1)))
} else {
return nil
}
}
func (this *generator) getInterfaceBehaviorSignature (ty *entity.TypeInterface, name string) (llvm.Type, error) {
return this.generateTypeFunction(ty.BehaviorMap[name])
}
func (this *generator) getInterfaceBehaviorIndex (ty *entity.TypeInterface, name string) int {
offset := -1
for index, nm := range ty.BehaviorOrder {
if nm == name {
offset = index
break
}
}
return offset
}
func (this *generator) getStructMemberIndex (ty *entity.TypeStruct, name string) int {
offset := -1
for index, nm := range ty.MemberOrder {
if nm == name {
offset = index
break
}
}
return offset
}

4
generator/doc.go Normal file
View File

@ -0,0 +1,4 @@
// Package generator implements the code generation stage of the FSPL compiler.
// It converts a well formed semantic tree into LLVM IR code, and outputs it to
// an io.Writer.
package generator

162
generator/expression-loc.go Normal file
View File

@ -0,0 +1,162 @@
package generator
import "fmt"
import "git.tebibyte.media/fspl/fspl/llvm"
import "git.tebibyte.media/fspl/fspl/entity"
import "git.tebibyte.media/fspl/fspl/analyzer"
func (this *generator) generateVariableLoc (variable *entity.Variable) (llvm.Value, error) {
return this.blockManager.variable(variable.Declaration)
}
func (this *generator) generateDeclarationLoc (declaration *entity.Declaration) (llvm.Value, error) {
return this.blockManager.addDeclaration(declaration, nil)
}
func (this *generator) generateSliceLoc (slice *entity.Slice) (llvm.Value, error) {
var start, end, dataAddress llvm.Value
var elementType llvm.Type
sizeType, err := this.generateTypeIndex()
if err != nil { return nil, err }
if slice.Start != nil {
start, err = this.generateExpressionVal(slice.Start)
if err != nil { return nil, err }
}
if slice.End != nil {
end, err = this.generateExpressionVal(slice.End)
if err != nil { return nil, err }
}
sourceType := analyzer.ReduceToBase(slice.Slice.Type())
switch sourceType := sourceType.(type) {
case *entity.TypeSlice:
source, err := this.generateExpressionLoc(slice.Slice)
if err != nil { return nil, err }
irSourceType, err := this.generateType(sourceType)
if err != nil { return nil, err }
elementType, err = this.generateType(sourceType.Element)
if err != nil { return nil, err }
dataAddressFieldAddress := this.blockManager.NewGetElementPtr (
irSourceType,
source,
llvm.NewConstInt(llvm.I32, 0),
llvm.NewConstInt(llvm.I32, 0))
dataAddress = this.blockManager.NewLoad (
new(llvm.TypePointer),
dataAddressFieldAddress)
lengthAddress := this.blockManager.NewGetElementPtr (
irSourceType,
source,
llvm.NewConstInt(llvm.I32, 0),
llvm.NewConstInt(llvm.I32, 1))
if end == nil {
end = this.blockManager.NewLoad(sizeType, lengthAddress)
}
case *entity.TypeArray:
elementType, err = this.generateType(sourceType.Element)
if err != nil { return nil, err }
dataAddress, err = this.generateExpressionLoc(slice.Slice)
if err != nil { return nil, err }
if end == nil {
end = llvm.NewConstInt(sizeType, int64(sourceType.Length))
}
default:
panic(fmt.Sprint (
"BUG: generator can't slice expression ",
slice.Slice))
}
// figure out starting position and length
var length llvm.Value
if start == nil {
length = end
} else {
length = this.blockManager.NewSub(end, start)
dataAddress = this.blockManager.NewGetElementPtr (
elementType,
dataAddress,
start)
}
// create new slice descriptor
return this.generateSlice(slice.Type(), dataAddress, length)
}
func (this *generator) generateSubscriptLoc (subscript *entity.Subscript) (llvm.Value, error) {
source, err := this.generateExpressionLoc(subscript.Slice)
if err != nil { return nil, err }
offset, err := this.generateExpressionVal(subscript.Offset)
if err != nil { return nil, err }
var elementType entity.Type
var dataAddress llvm.Value
sourceType := analyzer.ReduceToBase(subscript.Slice.Type())
switch sourceType := sourceType.(type) {
case *entity.TypeSlice:
irSourceType, err := this.generateType(subscript.Slice.Type())
if err != nil { return nil, err }
elementType = sourceType.Element
dataAddress = this.getSliceDataAddress(source, irSourceType)
case *entity.TypeArray:
elementType = sourceType.Element
dataAddress = source
default:
panic(fmt.Sprint (
"BUG: generator can't subscript expression ",
subscript.Slice))
}
// get element
irElementType, err := this.generateType(elementType)
if err != nil { return nil, err }
return this.blockManager.NewGetElementPtr (
irElementType,
dataAddress,
offset), nil
}
func (this *generator) generateDereferenceLoc (dereference *entity.Dereference) (llvm.Value, error) {
return this.generateExpressionVal(dereference.Pointer)
}
func (this *generator) generateMemberAccessLoc (access *entity.MemberAccess) (llvm.Value, error) {
source, err := this.generateExpressionLoc(access.Source)
if err != nil { return nil, err }
switch sourceType := analyzer.ReduceToBase(access.Source.Type()).(type) {
case *entity.TypeStruct:
irSourceType, err := this.generateType(access.Source.Type())
if err != nil { return nil, err }
offset := this.getStructMemberIndex(sourceType, access.Member)
return this.blockManager.NewGetElementPtr (
irSourceType,
source,
llvm.NewConstInt(llvm.I32, 0),
llvm.NewConstInt(llvm.I32, int64(offset))), nil
case *entity.TypePointer:
irSourceType, err := this.generateType(sourceType.Referenced)
if err != nil { return nil, err }
referencedSourceType := analyzer.ReduceToBase(sourceType.Referenced).(*entity.TypeStruct)
offset := this.getStructMemberIndex(referencedSourceType, access.Member)
source = this.blockManager.NewLoad(new(llvm.TypePointer), source)
return this.blockManager.NewGetElementPtr (
irSourceType,
source,
llvm.NewConstInt(llvm.I32, 0),
llvm.NewConstInt(llvm.I32, int64(offset))), nil
default:
panic(fmt.Sprint (
"BUG: generator can't access members in expression ",
access.Source))
}
}

View File

@ -0,0 +1,247 @@
package generator
import "fmt"
import "git.tebibyte.media/fspl/fspl/llvm"
import "git.tebibyte.media/fspl/fspl/entity"
type resultMode int; const (
resultModeAny resultMode = iota
resultModeVal
resultModeLoc
)
func (mode resultMode) String () string {
switch(mode) {
case resultModeAny: return "resultModeAny"
case resultModeVal: return "resultModeVal"
case resultModeLoc: return "resultModeLoc"
default: return fmt.Sprintf("resultMode(%d)", mode)
}
}
func (this *generator) valueToLocation (value llvm.Value) llvm.Value {
pointer := this.blockManager.newAllocaFront(value.Type())
this.blockManager.NewStore(value, pointer)
return pointer
}
func (this *generator) locationToValue (pointer llvm.Value, ty entity.Type) (llvm.Value, error) {
irType, err := this.generateType(ty)
if err != nil { return nil, err }
return this.blockManager.NewLoad(irType, pointer), nil
}
func (this *generator) generateExpression (
expression entity.Expression,
mode resultMode,
) (
register llvm.Value,
location bool,
err error,
) {
switch mode {
case resultModeAny:
return this.generateExpressionAny(expression)
case resultModeVal:
val, err := this.generateExpressionVal(expression)
return val, false, err
case resultModeLoc:
loc, err := this.generateExpressionLoc(expression)
return loc, true, err
default: panic("unknown resultMode")
}
}
// generateExpression generates an expression and either returns a register
// representing the result, or a register containing the location of the result,
// whichever will generate the least IR. In the latter case, the boolean
// "location" will be true.
func (this *generator) generateExpressionAny (expression entity.Expression) (register llvm.Value, location bool, err error) {
switch expression := expression.(type) {
// these give us an address
case *entity.Dereference,
*entity.MemberAccess,
*entity.Slice,
*entity.Subscript,
*entity.LiteralArray,
*entity.LiteralString,
*entity.LiteralStruct,
*entity.Variable,
*entity.Declaration:
pointer, err := this.generateExpressionLoc(expression)
return pointer, true, err
// these give us a value
case *entity.Call,
*entity.MethodCall,
*entity.Reference,
*entity.Length,
*entity.ValueCast,
*entity.BitCast,
*entity.Operation,
*entity.LiteralInt,
*entity.LiteralFloat,
*entity.LiteralBoolean,
*entity.LiteralNil:
value, err := this.generateExpressionVal(expression)
return value, true, err
// these are capable of giving us both
case *entity.Block:
return this.generateBlock(expression, resultModeAny)
case *entity.IfElse:
return this.generateIfElse(expression, resultModeAny)
case *entity.Loop:
return this.generateLoop(expression, resultModeAny)
// we get nothing from these
case *entity.Assignment:
_, err := this.generateAssignment(expression)
return nil, false, err
case *entity.Break:
_, err := this.generateBreak(expression)
return nil, false, err
case *entity.Return:
_, err := this.generateReturn(expression)
return nil, false, err
default:
panic(fmt.Sprintf (
"BUG: generator doesnt know about expression %v, ty: %T",
expression, expression))
}
}
// generateExpressionVal generates an expression and returns a register
// representing the result.
func (this *generator) generateExpressionVal (expression entity.Expression) (llvm.Value, error) {
// we get an address from these, so we need to load the value
switch expression := expression.(type) {
case *entity.Dereference,
*entity.MemberAccess,
*entity.Slice,
*entity.Subscript,
*entity.LiteralArray,
*entity.LiteralString,
*entity.LiteralStruct,
*entity.Variable,
*entity.Declaration:
pointer, err := this.generateExpressionLoc(expression)
if err != nil { return nil, err }
return this.locationToValue(pointer, expression.Type())
// we get a value from these, so we can return them as-is
case *entity.Call:
return this.generateCallVal(expression)
case *entity.MethodCall:
return this.generateMethodCallVal(expression)
case *entity.Reference:
return this.generateReferenceVal(expression)
case *entity.Length:
return this.generateLengthVal(expression)
case *entity.ValueCast:
return this.generateValueCastVal(expression)
case *entity.BitCast:
return this.generateBitCastVal(expression)
case *entity.Operation:
return this.generateOperationVal(expression)
case *entity.Block:
loc, _, err := this.generateBlock(expression, resultModeVal)
return loc, err
case *entity.IfElse:
loc, _, err := this.generateIfElse(expression, resultModeVal)
return loc, err
case *entity.Loop:
loc, _, err := this.generateLoop(expression, resultModeVal)
return loc, err
case *entity.LiteralInt:
return this.generateLiteralInt(expression)
case *entity.LiteralFloat:
return this.generateLiteralFloat(expression)
case *entity.LiteralBoolean:
return this.generateLiteralBoolean(expression)
case *entity.LiteralNil:
return this.generateLiteralNil(expression)
// we get nothing from these
case *entity.Assignment:
return this.generateAssignment(expression)
case *entity.Break:
return this.generateBreak(expression)
case *entity.Return:
return this.generateReturn(expression)
default:
panic(fmt.Sprintf (
"BUG: generator doesnt know about value expression %v, ty: %T",
expression, expression))
}
}
// generateExpressionLoc generates an expression and returns a register
// representing the address of the result. This function avoids adding alloca
// instructions whenever possible, trying to refer to the original data.
func (this *generator) generateExpressionLoc (expression entity.Expression) (llvm.Value, error) {
switch expression := expression.(type) {
// we get a value from these, so we need to store the value and then
// return an address to that
case *entity.Call,
*entity.MethodCall,
*entity.Reference,
*entity.Length,
*entity.ValueCast,
*entity.BitCast,
*entity.Operation,
*entity.LiteralInt,
*entity.LiteralFloat,
*entity.LiteralBoolean,
*entity.LiteralNil:
value, err := this.generateExpressionVal(expression)
if err != nil { return nil, err }
return this.valueToLocation(value), nil
// we get an address from these, so we can return them as-is
case *entity.Variable:
return this.generateVariableLoc(expression)
case *entity.Declaration:
return this.generateDeclarationLoc(expression)
case *entity.Slice:
return this.generateSliceLoc(expression)
case *entity.Subscript:
return this.generateSubscriptLoc(expression)
case *entity.Dereference:
return this.generateDereferenceLoc(expression)
case *entity.MemberAccess:
return this.generateMemberAccessLoc(expression)
case *entity.Block:
loc, _, err := this.generateBlock(expression, resultModeLoc)
return loc, err
case *entity.IfElse:
loc, _, err := this.generateIfElse(expression, resultModeLoc)
return loc, err
case *entity.Loop:
loc, _, err := this.generateLoop(expression, resultModeLoc)
return loc, err
case *entity.LiteralArray:
return this.generateLiteralArrayLoc(expression, nil)
case *entity.LiteralString:
return this.generateLiteralStringLoc(expression, nil)
case *entity.LiteralStruct:
return this.generateLiteralStructLoc(expression, nil)
// we get nothing from these
case *entity.Assignment:
return this.generateAssignment(expression)
case *entity.Break:
return this.generateBreak(expression)
case *entity.Return:
return this.generateReturn(expression)
default:
panic(fmt.Sprintf (
"BUG: generator doesnt know about location expression %v, ty: %T",
expression, expression))
}
}

649
generator/expression-val.go Normal file
View File

@ -0,0 +1,649 @@
package generator
import "fmt"
import "errors"
import "git.tebibyte.media/fspl/fspl/llvm"
import "git.tebibyte.media/fspl/fspl/entity"
import "git.tebibyte.media/fspl/fspl/analyzer"
func (this *generator) generateCallVal (call *entity.Call) (llvm.Value, error) {
function, err := this.function(entity.Key {
Unit: call.Unit,
Name: call.Name,
})
if err != nil { return nil, err }
args := make([]llvm.Value, len(call.Arguments))
for index, argument := range call.Arguments {
irArgument, err := this.generateAssignmentToDestination (
argument,
call.Function.Signature.Arguments[index].Type(),
nil)
if err != nil { return nil, err }
args[index] = irArgument
}
return this.blockManager.NewCall(function, function.Signature, args...), nil
}
func (this *generator) generateMethodCallVal (call *entity.MethodCall) (llvm.Value, error) {
var signature *entity.Signature
if call.Method != nil { signature = call.Method.Signature }
if call.Behavior != nil { signature = call.Behavior }
// build list of args
args := make([]llvm.Value, len(call.Arguments) + 1)
for index, argument := range call.Arguments {
irArgument, err := this.generateAssignmentToDestination (
argument,
signature.Arguments[index].Type(),
nil)
if err != nil { return nil, err }
args[index + 1] = irArgument
}
// check for methods on named type
if sourceType, ok := call.Source.Type().(*entity.TypeNamed); ok {
methodKey := entity.Key {
Unit: sourceType.Type.Unit(),
Name: sourceType.Name,
Method: call.Name,
}
method, err := this.method(methodKey)
if _, notFound := err.(errNotFound); !notFound {
source, err := this.generateExpressionLoc(call.Source)
if err != nil { return nil, err }
args[0] = source
return this.blockManager.NewCall(method, method.Signature, args...), nil
}
}
// check for methods on pointer to named type
if pointerType, ok := call.Source.Type().(*entity.TypePointer); ok {
if sourceType, ok := pointerType.Referenced.(*entity.TypeNamed); ok {
method, err := this.method(entity.Key {
Unit: sourceType.Unit(),
Name: sourceType.Name,
Method: call.Name,
})
if _, notFound := err.(errNotFound); !notFound {
source, err := this.generateExpressionVal(call.Source)
if err != nil { return nil, err }
args[0] = source
return this.blockManager.NewCall(method, method.Signature, args...), nil
}
}
}
// check for interface behaviors
if sourceType := getInterface(call.Source.Type()); sourceType != nil {
irSourceType, err := this.generateType(call.Source.Type())
if err != nil { return nil, err }
source, err := this.generateExpressionLoc(call.Source)
if err != nil { return nil, err }
irBehavior := this.getInterfaceBehaviorFieldLoc(sourceType, source, irSourceType, call.Name)
if irBehavior != nil {
irValue := this.getInterfaceDataFieldLoc(source, irSourceType)
signature, err := this.getInterfaceBehaviorSignature(sourceType, call.Name)
if err != nil { return nil, err }
args[0] = this.blockManager.NewLoad(irValue.Type(), irValue)
irBehavior := this.blockManager.NewLoad(llvm.Pointer, irBehavior)
return this.blockManager.NewCall (
irBehavior,
signature.(*llvm.TypeFunction),
args...), nil
}
}
return nil, errors.New(fmt.Sprintln("no method", call.Name, "for", call.Source))
}
func (this *generator) generateReferenceVal (reference *entity.Reference) (llvm.Value, error) {
return this.generateExpressionLoc(reference.Value)
}
func (this *generator) generateLengthVal (length *entity.Length) (llvm.Value, error) {
sizeType, err := this.generateTypeIndex()
if err != nil { return nil, err }
source, err := this.generateExpressionLoc(length.Slice)
if err != nil { return nil, err }
sourceType := analyzer.ReduceToBase(length.Slice.Type())
switch sourceType := sourceType.(type) {
case *entity.TypeSlice:
irSourceType, err := this.generateType(sourceType)
if err != nil { return nil, err }
lengthFieldAddress := this.blockManager.NewGetElementPtr (
irSourceType,
source,
llvm.NewConstInt(llvm.I32, 0),
llvm.NewConstInt(llvm.I32, 1))
return this.blockManager.NewLoad(sizeType, lengthFieldAddress), nil
case *entity.TypeArray:
return llvm.NewConstInt(sizeType, int64(sourceType.Length)), nil
default:
panic(fmt.Sprint (
"BUG: generator can't get length of expression ",
length.Slice))
}
}
func (this *generator) generateValueCastVal (cast *entity.ValueCast) (llvm.Value, error) {
generateFrom := func () (llvm.Value, error) {
return this.generateExpressionVal(cast.Value)
}
generateFromLoc := func () (llvm.Value, error) {
return this.generateExpressionLoc(cast.Value)
}
irFromType, err := this.generateType(cast.Value.Type())
if err != nil { return nil, err }
irToType, err := this.generateType(cast.Type())
if err != nil { return nil, err }
// Attempt to short circuit if types are equal to avoid LLVM bitching at
// us
if llvm.TypesEqual(irFromType, irToType) {
return this.generateExpressionVal(cast.Value)
}
fail := func () (llvm.Value, error) {
return nil, errors.New(fmt.Sprintf (
"cant convert %v to %v",
cast.Value.Type(), cast.Type()))
}
fromType := analyzer.ReduceToBase(cast.Value.Type())
toType := analyzer.ReduceToBase(cast.Type())
switch toType.(type) {
case *entity.TypeInt, *entity.TypeWord:
from, err := generateFrom()
if err != nil { return nil, err }
switch fromType := fromType.(type) {
case *entity.TypeInt, *entity.TypeWord:
// cast from integer to integer:
// basic truncated value assignment
return this.blockManager.NewTrunc(from, irToType), nil
case *entity.TypeFloat:
if analyzer.IsUnsigned(fromType) {
// cast from float to unsigned integer:
// convert float to unsigned integer
return this.blockManager.NewFPToUI(from, irToType), nil
} else {
// cast from float to signed integer:
// convert float to signed integer
return this.blockManager.NewFPToSI(from, irToType), nil
}
default: return fail()
}
case *entity.TypeFloat:
from, err := generateFrom()
if err != nil { return nil, err }
switch fromType := fromType.(type) {
case *entity.TypeFloat:
// cast from float to float:
// basic truncated value assignment
return this.blockManager.NewFPTrunc(from, irToType), nil
case *entity.TypeInt, *entity.TypeWord:
if analyzer.IsUnsigned(fromType) {
// cast from unsigned integer to float:
// convert unsigned integer to float
return this.blockManager.NewUIToFP(from, irToType), nil
} else {
// cast from signed integer to float:
// convert signed integer to float
return this.blockManager.NewSIToFP(from, irToType), nil
}
default: return fail()
}
case *entity.TypePointer:
switch fromType := fromType.(type) {
case *entity.TypeSlice:
// cast from slice to pointer:
// pointer will point to the first element of the slice
from, err := generateFromLoc()
if err != nil { return nil, err }
irFromType, err := this.generateType(fromType)
return this.getSliceDataAddress(from, irFromType), nil
case *entity.TypePointer:
// cast from pointer to pointer:
// basic forceful value assignment
// (all IR pointer types are equal)
return this.generateExpressionVal(cast.Value)
default: return fail()
}
case *entity.TypeSlice:
switch fromType.(type) {
case *entity.TypeSlice:
// cast from slice to slice:
// basic forceful value assignment
// (assume structural equivalence)
return this.generateExpressionVal(cast.Value)
default: return fail()
}
case *entity.TypeArray:
switch fromType.(type) {
case *entity.TypeArray:
// cast from array to array:
// basic forceful value assignment
// (assume structural equivalence)
return this.generateExpressionVal(cast.Value)
default: return fail()
}
case *entity.TypeStruct:
switch fromType.(type) {
case *entity.TypeArray:
// cast from struct to struct:
// basic forceful value assignment
// (assume structural equivalence)
return this.generateExpressionVal(cast.Value)
default: return fail()
}
default: return fail()
}
}
func (this *generator) generateBitCastVal (cast *entity.BitCast) (llvm.Value, error) {
irFromType, err := this.generateType(cast.Value.Type())
if err != nil { return nil, err }
irToType, err := this.generateType(cast.Type())
if err != nil { return nil, err }
// attempt to short circuit if types are equal, or else LLVM complains
if llvm.TypesEqual(irFromType, irToType) {
return this.generateExpressionVal(cast.Value)
}
from, err := this.generateExpressionVal(cast.Value)
if err != nil { return nil, err }
// determine how to *bit cast*. because of course its not that simple.
// thanks LLVM!
fromType := analyzer.ReduceToBase(cast.Value.Type())
toType := analyzer.ReduceToBase(cast.Type())
switch toType.(type) {
case *entity.TypeInt, *entity.TypeWord:
switch fromType.(type) {
case *entity.TypePointer:
// cast from pointer to int
return this.blockManager.NewPtrToInt(from, irToType), nil
}
case *entity.TypePointer:
switch fromType.(type) {
case *entity.TypeInt, *entity.TypeWord:
// cast from int to pointer
return this.blockManager.NewIntToPtr(from, irToType), nil
default:
// cast from something else to pointer
irIndexType, err := this.generateTypeIndex()
if err != nil { return nil, err }
temporaryInteger := this.blockManager.NewBitCast(from, irIndexType)
return this.blockManager.NewIntToPtr(temporaryInteger, irToType), nil
}
default:
switch fromType.(type) {
case *entity.TypePointer:
// cast from pointer to something else
irIndexType, err := this.generateTypeIndex()
if err != nil { return nil, err }
temporaryInteger := this.blockManager.NewPtrToInt(from, irIndexType)
return this.blockManager.NewBitCast(temporaryInteger, irToType), nil
}
}
// default to a normal bit cast
return this.blockManager.NewBitCast(from, irToType), nil
}
func (this *generator) generateOperationVal (operation *entity.Operation) (llvm.Value, error) {
nSameType := func (binary func (x, y llvm.Value) llvm.Value) (llvm.Value, error) {
irX, err := this.generateExpressionVal(operation.Arguments[0])
if err != nil { return nil, err }
for _, argument := range operation.Arguments[1:] {
irY, err := this.generateExpressionVal(argument)
if err != nil { return nil, err }
irX = binary(irX, irY)
}
return irX, nil
}
ty := analyzer.ReduceToBase(operation.Arguments[0].Type())
irType, err := this.generateType(ty)
if err != nil { return nil, err }
switch operation.Operator {
// math
case entity.OperatorAdd:
switch ty.(type) {
case *entity.TypeInt, *entity.TypeWord:
return nSameType(func (x, y llvm.Value) llvm.Value {
return this.blockManager.NewAdd(x, y)
})
case *entity.TypeFloat:
return nSameType(func (x, y llvm.Value) llvm.Value {
return this.blockManager.NewFAdd(x, y)
})
}
case entity.OperatorSubtract:
switch ty.(type) {
case *entity.TypeInt, *entity.TypeWord:
return nSameType(func (x, y llvm.Value) llvm.Value {
return this.blockManager.NewSub(x, y)
})
case *entity.TypeFloat:
return nSameType(func (x, y llvm.Value) llvm.Value {
return this.blockManager.NewFSub(x, y)
})
}
case entity.OperatorMultiply:
switch ty.(type) {
case *entity.TypeInt, *entity.TypeWord:
return nSameType(func (x, y llvm.Value) llvm.Value {
return this.blockManager.NewMul(x, y)
})
case *entity.TypeFloat:
return nSameType(func (x, y llvm.Value) llvm.Value {
return this.blockManager.NewFMul(x, y)
})
}
case entity.OperatorDivide:
switch ty.(type) {
case *entity.TypeInt, *entity.TypeWord:
if analyzer.IsUnsigned(ty) {
return nSameType(func (x, y llvm.Value) llvm.Value {
return this.blockManager.NewUDiv(x, y)
})
} else {
return nSameType(func (x, y llvm.Value) llvm.Value {
return this.blockManager.NewSDiv(x, y)
})
}
case *entity.TypeFloat:
return nSameType(func (x, y llvm.Value) llvm.Value {
return this.blockManager.NewFDiv(x, y)
})
}
case entity.OperatorIncrement:
irX, err := this.generateExpressionVal(operation.Arguments[0])
if err != nil { return nil, err }
irType := irType.(*llvm.TypeInt)
switch ty.(type) {
case *entity.TypeInt, *entity.TypeWord:
return this.blockManager.NewAdd (
irX,
llvm.NewConstInt(irType, 1)), nil
case *entity.TypeFloat:
return this.blockManager.NewFAdd (
irX,
llvm.NewConstInt(irType, 1)), nil
}
case entity.OperatorDecrement:
irX, err := this.generateExpressionVal(operation.Arguments[0])
if err != nil { return nil, err }
irType := irType.(*llvm.TypeInt)
switch ty.(type) {
case *entity.TypeInt, *entity.TypeWord:
return this.blockManager.NewSub (
irX,
llvm.NewConstInt(irType, 1)), nil
case *entity.TypeFloat:
return this.blockManager.NewFSub (
irX,
llvm.NewConstInt(irType, 1)), nil
}
case entity.OperatorModulo:
switch ty.(type) {
case *entity.TypeInt, *entity.TypeWord:
if analyzer.IsUnsigned(ty) {
return nSameType(func (x, y llvm.Value) llvm.Value {
return this.blockManager.NewURem(x, y)
})
} else {
return nSameType(func (x, y llvm.Value) llvm.Value {
return this.blockManager.NewSRem(x, y)
})
}
case *entity.TypeFloat:
return nSameType(func (x, y llvm.Value) llvm.Value {
return this.blockManager.NewFDiv(x, y)
})
}
// logic
case entity.OperatorLogicalNot:
irX, err := this.generateExpressionVal(operation.Arguments[0])
if err != nil { return nil, err }
return this.blockManager.NewICmp(llvm.IPredicateEQ, irX, llvm.NewConstBool(false)), nil
case entity.OperatorLogicalOr:
incomings := make([]*llvm.Incoming, len(operation.Arguments) + 1)
exit := this.blockManager.newBlock()
for index, argument := range operation.Arguments {
irX, err := this.generateExpressionVal(argument)
if err != nil { return nil, err }
incomings[index] = &llvm.Incoming {
X: llvm.NewConstBool(true),
Predecessor: this.blockManager.Block,
}
previous := this.blockManager.Block
block := this.blockManager.newBlock()
previous.NewCondBr(irX, exit, block)
}
this.blockManager.NewBr(exit)
incomings[len(incomings) - 1] = &llvm.Incoming {
X: llvm.NewConstBool(false),
Predecessor: this.blockManager.Block,
}
this.blockManager.Block = exit
return this.blockManager.NewPhi(incomings...), nil
case entity.OperatorLogicalAnd:
incomings := make([]*llvm.Incoming, len(operation.Arguments) + 1)
exit := this.blockManager.newBlock()
for index, argument := range operation.Arguments {
irX, err := this.generateExpressionVal(argument)
if err != nil { return nil, err }
incomings[index] = &llvm.Incoming {
X: llvm.NewConstBool(false),
Predecessor: this.blockManager.Block,
}
previous := this.blockManager.Block
block := this.blockManager.newBlock()
previous.NewCondBr(irX, block, exit)
}
this.blockManager.NewBr(exit)
incomings[len(incomings) - 1] = &llvm.Incoming {
X: llvm.NewConstBool(true),
Predecessor: this.blockManager.Block,
}
this.blockManager.Block = exit
return this.blockManager.NewPhi(incomings...), nil
case entity.OperatorLogicalXor:
return nSameType(func (x, y llvm.Value) llvm.Value {
return this.blockManager.NewICmp (
llvm.IPredicateNE,
this.blockManager.NewICmp(llvm.IPredicateEQ, x, llvm.NewConstBool(true)),
this.blockManager.NewICmp(llvm.IPredicateEQ, x, llvm.NewConstBool(true)))
})
// bit manipulation
case entity.OperatorNot:
irX, err := this.generateExpressionVal(operation.Arguments[0])
if err != nil { return nil, err }
return this.blockManager.NewXor (
irX,
llvm.NewConstInt(irType.(*llvm.TypeInt), -1)), nil
case entity.OperatorOr:
return nSameType(func (x, y llvm.Value) llvm.Value {
return this.blockManager.NewOr(x, y)
})
case entity.OperatorAnd:
return nSameType(func (x, y llvm.Value) llvm.Value {
return this.blockManager.NewAnd(x, y)
})
case entity.OperatorXor:
return nSameType(func (x, y llvm.Value) llvm.Value {
return this.blockManager.NewXor(x, y)
})
case entity.OperatorLeftShift:
irX, err := this.generateExpressionVal(operation.Arguments[0])
if err != nil { return nil, err }
irY, err := this.generateExpressionVal(operation.Arguments[1])
if err != nil { return nil, err }
return this.blockManager.NewShl(irX, irY), nil
case entity.OperatorRightShift:
irX, err := this.generateExpressionVal(operation.Arguments[0])
if err != nil { return nil, err }
irY, err := this.generateExpressionVal(operation.Arguments[1])
if err != nil { return nil, err }
if analyzer.IsUnsigned(ty) {
return this.blockManager.NewLShr(irX, irY), nil
} else {
return this.blockManager.NewAShr(irX, irY), nil
}
// comparison
case entity.OperatorLess:
switch ty.(type) {
case *entity.TypeInt, *entity.TypeWord:
var predicate llvm.IPredicate
if analyzer.IsUnsigned(ty) {
predicate = llvm.IPredicateULT
} else {
predicate = llvm.IPredicateSLT
}
return nSameType(func (x, y llvm.Value) llvm.Value {
return this.blockManager.NewICmp(predicate, x, y)
})
case *entity.TypeFloat:
return nSameType(func (x, y llvm.Value) llvm.Value {
return this.blockManager.NewFCmp (
llvm.FPredicateOLT,
x, y)
})
}
case entity.OperatorGreater:
switch ty.(type) {
case *entity.TypeInt, *entity.TypeWord:
var predicate llvm.IPredicate
if analyzer.IsUnsigned(ty) {
predicate = llvm.IPredicateUGT
} else {
predicate = llvm.IPredicateSGT
}
return nSameType(func (x, y llvm.Value) llvm.Value {
return this.blockManager.NewICmp(predicate, x, y)
})
case *entity.TypeFloat:
return nSameType(func (x, y llvm.Value) llvm.Value {
return this.blockManager.NewFCmp (
llvm.FPredicateOGT,
x, y)
})
}
case entity.OperatorLessEqual:
switch ty.(type) {
case *entity.TypeInt, *entity.TypeWord:
var predicate llvm.IPredicate
if analyzer.IsUnsigned(ty) {
predicate = llvm.IPredicateULE
} else {
predicate = llvm.IPredicateSLE
}
return nSameType(func (x, y llvm.Value) llvm.Value {
return this.blockManager.NewICmp(predicate, x, y)
})
case *entity.TypeFloat:
return nSameType(func (x, y llvm.Value) llvm.Value {
return this.blockManager.NewFCmp (
llvm.FPredicateOLE,
x, y)
})
}
case entity.OperatorGreaterEqual:
switch ty.(type) {
case *entity.TypeInt, *entity.TypeWord:
var predicate llvm.IPredicate
if analyzer.IsUnsigned(ty) {
predicate = llvm.IPredicateUGE
} else {
predicate = llvm.IPredicateSGE
}
return nSameType(func (x, y llvm.Value) llvm.Value {
return this.blockManager.NewICmp(predicate, x, y)
})
case *entity.TypeFloat:
return nSameType(func (x, y llvm.Value) llvm.Value {
return this.blockManager.NewFCmp (
llvm.FPredicateOGE,
x, y)
})
}
case entity.OperatorEqual:
switch ty.(type) {
case *entity.TypeInt, *entity.TypeWord:
return nSameType(func (x, y llvm.Value) llvm.Value {
return this.blockManager.NewICmp (
llvm.IPredicateEQ,
x, y)
})
case *entity.TypeFloat:
return nSameType(func (x, y llvm.Value) llvm.Value {
return this.blockManager.NewFCmp (
llvm.FPredicateOEQ,
x, y)
})
}
default:
panic(fmt.Sprint (
"BUG: generator doesnt know about operator",
operation.Operator))
}
panic(fmt.Sprint (
"BUG: generator failed to generate operation",
operation))
}

117
generator/expression.go Normal file
View File

@ -0,0 +1,117 @@
package generator
// import "fmt"
import "git.tebibyte.media/fspl/fspl/llvm"
import "git.tebibyte.media/fspl/fspl/entity"
// import "git.tebibyte.media/fspl/fspl/analyzer"
func (this *generator) generateBlock (block *entity.Block, mode resultMode) (llvm.Value, bool, error) {
if len(block.Steps) == 0 { return nil, false, nil }
lastIndex := len(block.Steps) - 1
for _, step := range block.Steps[:lastIndex] {
_, _, err := this.generateExpressionAny(step)
if err != nil { return nil, false, err }
}
return this.generateExpression(block.Steps[lastIndex], mode)
}
func (this *generator) generateBreak (brk *entity.Break) (llvm.Value, error) {
loopEntry := this.blockManager.topLoop()
if brk.Value != nil {
value, loc, err := this.generateExpression(brk.Value, loopEntry.mode)
if err != nil { return nil, err }
loopEntry.value = value
loopEntry.loc = loc
}
loopEntry.stub = this.blockManager.Block
return nil, nil
}
func (this *generator) generateReturn (ret *entity.Return) (llvm.Value, error) {
if ret.Value == nil {
this.blockManager.NewRet(nil)
} else {
value, err := this.generateExpressionVal(ret.Value)
if err != nil { return nil, err }
this.blockManager.NewRet(value)
}
return nil, nil
}
func (this *generator) generateIfElse (ifelse *entity.IfElse, mode resultMode) (llvm.Value, bool, error) {
condition, err := this.generateExpressionVal(ifelse.Condition)
if err != nil { return nil, false, err }
previous := this.blockManager.Block
var trueBlock, falseBlock *llvm.Block
var tru, fals llvm.Value
var loc bool
trueBlock = this.blockManager.newBlock()
exitBlock := this.blockManager.newBlock()
this.blockManager.Block = trueBlock
tru, loc, err = this.generateExpression(ifelse.True, mode)
if err != nil { return nil, false, err }
if !this.blockManager.Terminated() { this.blockManager.NewBr(exitBlock) }
if ifelse.False == nil {
// there is no false case
previous.NewCondBr(condition, trueBlock, exitBlock)
this.blockManager.Block = exitBlock
return nil, loc, nil
} else {
// there is a false case
falseBlock = this.blockManager.newBlock()
fals, _, err = this.generateExpression(ifelse.False, mode)
if err != nil { return nil, false, err }
if !this.blockManager.Terminated() { this.blockManager.NewBr(exitBlock) }
if mode == resultModeAny {
// discard results of statements
previous.NewCondBr(condition, trueBlock, falseBlock)
this.blockManager.Block = exitBlock
return nil, false, nil
} else {
// obtain results of statements
// set up phi to capture results
trueIncoming := &llvm.Incoming {
X: tru,
Predecessor: trueBlock,
}
falseIncoming := &llvm.Incoming {
X: fals,
Predecessor: falseBlock,
}
previous.NewCondBr(condition, trueBlock, falseBlock)
this.blockManager.Block = exitBlock
return this.blockManager.NewPhi(trueIncoming, falseIncoming), loc, nil
}
}
}
func (this *generator) generateLoop (loop *entity.Loop, mode resultMode) (llvm.Value, bool, error) {
previous := this.blockManager.Block
body := this.blockManager.newBlock()
previous.NewBr(body)
loopEntry := this.blockManager.pushLoop(mode)
defer this.blockManager.popLoop()
_, _, err := this.generateExpressionAny(loop.Body)
if err != nil { return nil, false, err }
this.blockManager.NewBr(body)
exit := this.blockManager.newBlock()
if loopEntry.stub != nil {
loopEntry.stub.SetTerminator(&llvm.TerminatorBr {
Target: exit,
})
}
return loopEntry.value, loopEntry.loc, nil
}

106
generator/function.go Normal file
View File

@ -0,0 +1,106 @@
package generator
import "git.tebibyte.media/fspl/fspl/llvm"
import "git.tebibyte.media/fspl/fspl/entity"
func (this *generator) generateFunction (
function *entity.Function,
) (
*llvm.Function,
error,
) {
key := entity.Key {
Unit: function.Unit,
Name: function.Signature.Name,
}
params := make([]*llvm.Parameter, len(function.Signature.Arguments))
ret, err := this.generateType(function.Signature.Return)
if err != nil { return nil, err }
for index, argument := range function.Signature.Arguments {
paramType, err := this.generateType(argument.Type())
if err != nil { return nil, err }
params[index] = llvm.NewParameter(argument.Name, paramType)
}
var name string; if function.LinkName == "" {
name = key.LinkName()
} else {
name = function.LinkName
}
irFunc := this.module.NewFunction(name, ret, params...)
if function.Body != nil {
this.blockManager = this.pushBlockManager(irFunc)
defer this.popBlockManager()
this.blockManager.addFunctionArgumentDeclarations(function)
if function.Signature.Return == nil {
_, _, err := this.generateExpressionAny(function.Body)
if err != nil { return nil, err }
this.blockManager.NewRet(nil)
} else {
body, err := this.generateExpressionVal(function.Body)
if err != nil { return nil, err }
this.blockManager.NewRet(body)
}
}
this.functions[key] = irFunc
return irFunc, nil
}
func (this *generator) generateMethod (
method *entity.Method,
) (
*llvm.Function,
error,
) {
key := entity.Key {
Unit: method.Unit,
Name: method.TypeName,
Method: method.Signature.Name,
}
params := make([]*llvm.Parameter, len(method.Signature.Arguments) + 1)
ret, err := this.generateType(method.Signature.Return)
if err != nil { return nil, err }
for index, argument := range method.Signature.Arguments {
paramType, err := this.generateType(argument.Type())
if err != nil { return nil, err }
params[index + 1] = llvm.NewParameter(argument.Name, paramType)
}
params[0] = llvm.NewParameter("this", new(llvm.TypePointer))
var name string; if method.LinkName == "" {
name = key.LinkName()
} else {
name = method.LinkName
}
irFunc := this.module.NewFunction(name, ret, params...)
if method.Body != nil {
this.blockManager = this.pushBlockManager(irFunc)
defer this.popBlockManager()
this.blockManager.addFunctionArgumentDeclarations(method)
if method.Signature.Return == nil {
_, _, err := this.generateExpressionAny(method.Body)
if err != nil { return nil, err }
this.blockManager.NewRet(nil)
} else {
body, err := this.generateExpressionVal(method.Body)
if err != nil { return nil, err }
this.blockManager.NewRet(body)
}
}
this.functions[key] = irFunc
return irFunc, nil
}

238
generator/function_test.go Normal file
View File

@ -0,0 +1,238 @@
package generator
import "testing"
func TestFunctionExternal (test *testing.T) {
testString (test,
`declare void @exit(i64 %status)
define void @main() {
0:
call void @exit(i64 1)
ret void
}
`,
`
[exit status:Int] 'exit'
[main] 'main' = [exit 1]
`)
}
func TestFunctionCall (test *testing.T) {
testString (test,
`define i64 @"0zNZN147MN2wzMAQ6NS2dQ==::f"() {
0:
%1 = call i64 @"0zNZN147MN2wzMAQ6NS2dQ==::g"(i64 5)
ret i64 %1
}
define i64 @"0zNZN147MN2wzMAQ6NS2dQ==::g"(i64 %x) {
0:
%1 = alloca i64
store i64 %x, ptr %1
%2 = load i64, ptr %1
%3 = call i64 @"0zNZN147MN2wzMAQ6NS2dQ==::h"(i64 %2)
ret i64 %3
}
define i64 @"0zNZN147MN2wzMAQ6NS2dQ==::h"(i64 %x) {
0:
%1 = alloca i64
store i64 %x, ptr %1
%2 = load i64, ptr %1
ret i64 %2
}
`,
`
[f ]:Int = [g 5]
[g x:Int]:Int = [h x]
[h x:Int]:Int = x
`)
}
func TestFunctionCallVoid (test *testing.T) {
testString (test,
`define void @"0zNZN147MN2wzMAQ6NS2dQ==::f"() {
0:
%1 = call i64 @"0zNZN147MN2wzMAQ6NS2dQ==::g"(i64 5)
call void @"0zNZN147MN2wzMAQ6NS2dQ==::h"()
%2 = call i64 @"0zNZN147MN2wzMAQ6NS2dQ==::g"(i64 6)
ret void
}
define i64 @"0zNZN147MN2wzMAQ6NS2dQ==::g"(i64 %x) {
0:
%1 = alloca i64
store i64 %x, ptr %1
%2 = load i64, ptr %1
ret i64 %2
}
define void @"0zNZN147MN2wzMAQ6NS2dQ==::h"() {
0:
ret void
}
`,
`
[f] = { [g 5] [h] [g 6] }
[g x:Int]:Int = x
[h] = { }
`)
}
func TestMethod (test *testing.T) {
testString (test,
`%"0zNZN147MN2wzMAQ6NS2dQ==::Number" = type i64
define i64 @"0zNZN147MN2wzMAQ6NS2dQ==::main"() {
0:
%1 = alloca %"0zNZN147MN2wzMAQ6NS2dQ==::Number"
store i64 5, ptr %1
%2 = call i64 @"0zNZN147MN2wzMAQ6NS2dQ==::Number.number"(ptr %1)
ret i64 %2
}
define i64 @"0zNZN147MN2wzMAQ6NS2dQ==::Number.number"(ptr %this) {
0:
%1 = alloca ptr
store ptr %this, ptr %1
%2 = load ptr, ptr %1
%3 = load i64, ptr %2
ret i64 %3
}
`,
`
Number: Int
Number.[number]: Int = [.this]
[main]: Int = {
num:Number = 5
num.[number]
}
`)
}
func TestMethodChained (test *testing.T) {
testString (test,
`%"0zNZN147MN2wzMAQ6NS2dQ==::Number" = type i64
define %"0zNZN147MN2wzMAQ6NS2dQ==::Number" @"0zNZN147MN2wzMAQ6NS2dQ==::main"() {
0:
%1 = alloca i64
store i64 5, ptr %1
%2 = call %"0zNZN147MN2wzMAQ6NS2dQ==::Number" @"0zNZN147MN2wzMAQ6NS2dQ==::Number.add"(ptr %1, i64 8)
%3 = alloca %"0zNZN147MN2wzMAQ6NS2dQ==::Number"
store %"0zNZN147MN2wzMAQ6NS2dQ==::Number" %2, ptr %3
%4 = call %"0zNZN147MN2wzMAQ6NS2dQ==::Number" @"0zNZN147MN2wzMAQ6NS2dQ==::Number.mul"(ptr %3, i64 3)
ret %"0zNZN147MN2wzMAQ6NS2dQ==::Number" %4
}
define %"0zNZN147MN2wzMAQ6NS2dQ==::Number" @"0zNZN147MN2wzMAQ6NS2dQ==::Number.mul"(ptr %this, %"0zNZN147MN2wzMAQ6NS2dQ==::Number" %x) {
0:
%1 = alloca ptr
store ptr %this, ptr %1
%2 = alloca %"0zNZN147MN2wzMAQ6NS2dQ==::Number"
store %"0zNZN147MN2wzMAQ6NS2dQ==::Number" %x, ptr %2
%3 = load ptr, ptr %1
%4 = load %"0zNZN147MN2wzMAQ6NS2dQ==::Number", ptr %3
%5 = load %"0zNZN147MN2wzMAQ6NS2dQ==::Number", ptr %2
%6 = mul %"0zNZN147MN2wzMAQ6NS2dQ==::Number" %4, %5
ret %"0zNZN147MN2wzMAQ6NS2dQ==::Number" %6
}
define %"0zNZN147MN2wzMAQ6NS2dQ==::Number" @"0zNZN147MN2wzMAQ6NS2dQ==::Number.add"(ptr %this, %"0zNZN147MN2wzMAQ6NS2dQ==::Number" %x) {
0:
%1 = alloca ptr
store ptr %this, ptr %1
%2 = alloca %"0zNZN147MN2wzMAQ6NS2dQ==::Number"
store %"0zNZN147MN2wzMAQ6NS2dQ==::Number" %x, ptr %2
%3 = load ptr, ptr %1
%4 = load %"0zNZN147MN2wzMAQ6NS2dQ==::Number", ptr %3
%5 = load %"0zNZN147MN2wzMAQ6NS2dQ==::Number", ptr %2
%6 = add %"0zNZN147MN2wzMAQ6NS2dQ==::Number" %4, %5
ret %"0zNZN147MN2wzMAQ6NS2dQ==::Number" %6
}
define %"0zNZN147MN2wzMAQ6NS2dQ==::Number" @"0zNZN147MN2wzMAQ6NS2dQ==::Number.div"(ptr %this, %"0zNZN147MN2wzMAQ6NS2dQ==::Number" %x) {
0:
%1 = alloca ptr
store ptr %this, ptr %1
%2 = alloca %"0zNZN147MN2wzMAQ6NS2dQ==::Number"
store %"0zNZN147MN2wzMAQ6NS2dQ==::Number" %x, ptr %2
%3 = load ptr, ptr %1
%4 = load %"0zNZN147MN2wzMAQ6NS2dQ==::Number", ptr %3
%5 = load %"0zNZN147MN2wzMAQ6NS2dQ==::Number", ptr %2
%6 = sdiv %"0zNZN147MN2wzMAQ6NS2dQ==::Number" %4, %5
ret %"0zNZN147MN2wzMAQ6NS2dQ==::Number" %6
}
define %"0zNZN147MN2wzMAQ6NS2dQ==::Number" @"0zNZN147MN2wzMAQ6NS2dQ==::Number.sub"(ptr %this, %"0zNZN147MN2wzMAQ6NS2dQ==::Number" %x) {
0:
%1 = alloca ptr
store ptr %this, ptr %1
%2 = alloca %"0zNZN147MN2wzMAQ6NS2dQ==::Number"
store %"0zNZN147MN2wzMAQ6NS2dQ==::Number" %x, ptr %2
%3 = load ptr, ptr %1
%4 = load %"0zNZN147MN2wzMAQ6NS2dQ==::Number", ptr %3
%5 = load %"0zNZN147MN2wzMAQ6NS2dQ==::Number", ptr %2
%6 = sub %"0zNZN147MN2wzMAQ6NS2dQ==::Number" %4, %5
ret %"0zNZN147MN2wzMAQ6NS2dQ==::Number" %6
}
`,
`
Number: Int
Number.[add x:Number]:Number = [+ [.this] x]
Number.[sub x:Number]:Number = [- [.this] x]
Number.[mul x:Number]:Number = [* [.this] x]
Number.[div x:Number]:Number = [/ [.this] x]
[main]: Number = [~Number 5].[add 8].[mul 3]
`)
}
func TestMethodGreeter (test *testing.T) {
testString (test,
`%"AAAAAAAAAAAAAAAAAAAAAA==::Index" = type i64
%"AAAAAAAAAAAAAAAAAAAAAA==::String" = type { ptr, %"AAAAAAAAAAAAAAAAAAAAAA==::Index" }
%"0zNZN147MN2wzMAQ6NS2dQ==::Greeter" = type { %"AAAAAAAAAAAAAAAAAAAAAA==::String" }
define void @main() {
0:
%1 = alloca %"0zNZN147MN2wzMAQ6NS2dQ==::Greeter"
%2 = getelementptr %"0zNZN147MN2wzMAQ6NS2dQ==::Greeter", ptr %1, i32 0, i32 0
%3 = alloca [6 x i8]
%4 = getelementptr [6 x i8], ptr %3, i32 0, i32 0
store i8 104, ptr %4
%5 = getelementptr [6 x i8], ptr %3, i32 0, i32 1
store i8 101, ptr %5
%6 = getelementptr [6 x i8], ptr %3, i32 0, i32 2
store i8 108, ptr %6
%7 = getelementptr [6 x i8], ptr %3, i32 0, i32 3
store i8 108, ptr %7
%8 = getelementptr [6 x i8], ptr %3, i32 0, i32 4
store i8 111, ptr %8
%9 = getelementptr [6 x i8], ptr %3, i32 0, i32 5
store i8 0, ptr %9
%10 = getelementptr %"AAAAAAAAAAAAAAAAAAAAAA==::String", ptr %2, i32 0, i32 0
store ptr %3, ptr %10
%11 = getelementptr %"AAAAAAAAAAAAAAAAAAAAAA==::String", ptr %2, i32 0, i32 1
store %"AAAAAAAAAAAAAAAAAAAAAA==::Index" 6, ptr %11
call void @"0zNZN147MN2wzMAQ6NS2dQ==::Greeter.greet"(ptr %1)
ret void
}
define void @"0zNZN147MN2wzMAQ6NS2dQ==::Greeter.greet"(ptr %this) {
0:
%1 = alloca ptr
store ptr %this, ptr %1
%2 = load ptr, ptr %1
%3 = getelementptr %"0zNZN147MN2wzMAQ6NS2dQ==::Greeter", ptr %2, i32 0, i32 0
%4 = getelementptr %"AAAAAAAAAAAAAAAAAAAAAA==::String", ptr %3, i32 0, i32 0
%5 = load ptr, ptr %4
%6 = getelementptr i8, ptr %5, i64 0
%7 = call %"AAAAAAAAAAAAAAAAAAAAAA==::Index" @puts(ptr %6)
ret void
}
declare %"AAAAAAAAAAAAAAAAAAAAAA==::Index" @puts(ptr %string)
`,
`
[puts string: *Byte]: Index 'puts'
Greeter: (. message: String)
Greeter.[greet] = [puts [@[.this.message 0]]]
[main] 'main' = {
greeter: Greeter = (.
message: 'hello\0'
)
greeter.[greet]
}
`)
}

145
generator/generator.go Normal file
View File

@ -0,0 +1,145 @@
package generator
import "sort"
import "git.tebibyte.media/fspl/fspl/llvm"
import "git.tebibyte.media/fspl/fspl/entity"
import "git.tebibyte.media/fspl/fspl/analyzer"
type errNotFound string
func (err errNotFound) Error () string {
return string(err) + " not found"
}
// Target contains information about the machine the code is being written for.
type Target struct {
// WordSize is the size of the machine word. This determines the width
// of the Word type.
WordSize uint64
// Arch specifies the machine architecture
Arch string
}
type generator struct {
target Target
tree analyzer.Tree
module *llvm.Module
types map[entity.Key] llvm.Type
functions map[entity.Key] *llvm.Function
managerStack []*blockManager
blockManager *blockManager
}
// Generate takes in a semantic tree and writes corresponding LLVM IR to the
// given io.Writer. It returns an error in case there is something wrong with
// the semantic tree that prevents the code generation process from occurring.
func (this Target) Generate (tree analyzer.Tree) (*llvm.Module, error) {
return (&generator {
module: new(llvm.Module),
target: this,
tree: tree,
types: make(map[entity.Key] llvm.Type),
functions: make(map[entity.Key] *llvm.Function),
}).generate()
}
func (this *generator) generate () (*llvm.Module, error) {
// generate functions
functions := sortKeyedMapKeys(this.tree.Functions)
for _, key := range functions {
_, err := this.function(key)
if err != nil { return nil, err }
}
// generate methods
types := sortKeyedMapKeys(this.tree.Types)
for _, key := range types {
ty := this.tree.Types[key]
methods := sortMapKeys(ty.Methods)
for _, methodName := range methods {
methodKey := key
methodKey.Method = methodName
_, err := this.method(methodKey)
if err != nil { return nil, err }
}
}
return this.module, nil
}
func (this *generator) typedef (key entity.Key) (llvm.Type, error) {
ty, exists := this.types[key]
if exists { return ty, nil }
def, exists := this.tree.Types[key]
if !exists {
return nil, errNotFound("typedef " + key.String())
}
return this.generateTypedef(def)
}
func (this *generator) method (key entity.Key) (*llvm.Function, error) {
method, exists := this.functions[key]
if exists { return method, nil }
ty, exists := this.tree.Types[key.StripMethod()]
if !exists {
return nil, errNotFound("owner of method " + key.String())
}
if method, exists := ty.Methods[key.Method]; exists {
return this.generateMethod(method)
}
return nil, errNotFound("method " + key.String())
}
func (this *generator) function (key entity.Key) (*llvm.Function, error) {
function, exists := this.functions[key]
if exists { return function, nil }
if function, exists := this.tree.Functions[key]; exists {
return this.generateFunction(function)
}
return nil, errNotFound("function " + key.String())
}
func sortMapKeys[T any] (unsorted map[string] T) []string {
keys := make([]string, len(unsorted))
index := 0
for key := range unsorted {
keys[index] = key
index ++
}
sort.Strings(keys)
return keys
}
type keySlice []entity.Key
func (keys keySlice) Len () int {
return len(keys)
}
func (keys keySlice) Less (lefti, righti int) bool {
left := keys[lefti]
right := keys[righti]
return left.String() < right.String()
}
func (keys keySlice) Swap (lefti, righti int) {
keys[lefti], keys[righti] = keys[righti], keys[lefti]
}
func sortKeyedMapKeys[T any] (unsorted map[entity.Key] T) keySlice {
keys := make(keySlice, len(unsorted))
index := 0
for key := range unsorted {
keys[index] = key
index ++
}
sort.Sort(keys)
return keys
}

251
generator/interface_test.go Normal file
View File

@ -0,0 +1,251 @@
package generator
import "testing"
func TestInterfaceBasic (test *testing.T) {
testString (test,
`%"0zNZN147MN2wzMAQ6NS2dQ==::Doer" = type { ptr, ptr }
%"0zNZN147MN2wzMAQ6NS2dQ==::T" = type i64
define void @"0zNZN147MN2wzMAQ6NS2dQ==::main"() {
0:
%1 = alloca %"0zNZN147MN2wzMAQ6NS2dQ==::Doer"
%2 = getelementptr %"0zNZN147MN2wzMAQ6NS2dQ==::Doer", ptr %1, i32 0, i32 0
%3 = alloca %"0zNZN147MN2wzMAQ6NS2dQ==::T"
store ptr %3, ptr %2
%4 = getelementptr %"0zNZN147MN2wzMAQ6NS2dQ==::Doer", ptr %1, i32 0, i32 1
store ptr @"0zNZN147MN2wzMAQ6NS2dQ==::T.do", ptr %4
%5 = getelementptr %"0zNZN147MN2wzMAQ6NS2dQ==::Doer", ptr %1, i32 0, i32 1
%6 = getelementptr %"0zNZN147MN2wzMAQ6NS2dQ==::Doer", ptr %1, i32 0, i32 0
%7 = load ptr, ptr %6
%8 = load ptr, ptr %5
call void %8(ptr %7)
ret void
}
define void @"0zNZN147MN2wzMAQ6NS2dQ==::T.do"(ptr %this) {
0:
%1 = alloca ptr
store ptr %this, ptr %1
ret void
}
`,
`
Doer: (~ [do])
T: Int
T.[do] = { }
[main] = {
ifa:Doer = x:T
ifa.[do]
}
`)
}
func TestInterfaceIntegerReturn (test *testing.T) {
testString (test,
`%"0zNZN147MN2wzMAQ6NS2dQ==::Number" = type i64
%"0zNZN147MN2wzMAQ6NS2dQ==::Numbered" = type { ptr, ptr }
define i64 @"0zNZN147MN2wzMAQ6NS2dQ==::main"() {
0:
%1 = alloca %"0zNZN147MN2wzMAQ6NS2dQ==::Number"
store i64 5, ptr %1
%2 = alloca %"0zNZN147MN2wzMAQ6NS2dQ==::Numbered"
%3 = getelementptr %"0zNZN147MN2wzMAQ6NS2dQ==::Numbered", ptr %2, i32 0, i32 0
store ptr %1, ptr %3
%4 = getelementptr %"0zNZN147MN2wzMAQ6NS2dQ==::Numbered", ptr %2, i32 0, i32 1
store ptr @"0zNZN147MN2wzMAQ6NS2dQ==::Number.number", ptr %4
%5 = getelementptr %"0zNZN147MN2wzMAQ6NS2dQ==::Numbered", ptr %2, i32 0, i32 1
%6 = getelementptr %"0zNZN147MN2wzMAQ6NS2dQ==::Numbered", ptr %2, i32 0, i32 0
%7 = load ptr, ptr %6
%8 = load ptr, ptr %5
%9 = call i64 %8(ptr %7)
ret i64 %9
}
define i64 @"0zNZN147MN2wzMAQ6NS2dQ==::Number.number"(ptr %this) {
0:
%1 = alloca ptr
store ptr %this, ptr %1
%2 = load ptr, ptr %1
%3 = load i64, ptr %2
ret i64 %3
}
`,
`
Numbered: (~ [number]: Int)
Number: Int
Number.[number]: Int = [.this]
[main]: Int = {
num:Number = 5
ifa:Numbered = num
ifa.[number]
}
`)
}
func TestInterfaceWriter (test *testing.T) {
testString (test,
`%"0zNZN147MN2wzMAQ6NS2dQ==::File" = type i32
%"0zNZN147MN2wzMAQ6NS2dQ==::Writer" = type { ptr, ptr }
%"AAAAAAAAAAAAAAAAAAAAAA==::Index" = type i64
%"AAAAAAAAAAAAAAAAAAAAAA==::Byte" = type i8
define i64 @main() {
0:
%1 = alloca %"0zNZN147MN2wzMAQ6NS2dQ==::File"
store i32 0, ptr %1
%2 = alloca %"0zNZN147MN2wzMAQ6NS2dQ==::Writer"
%3 = getelementptr %"0zNZN147MN2wzMAQ6NS2dQ==::Writer", ptr %2, i32 0, i32 0
store ptr %1, ptr %3
%4 = getelementptr %"0zNZN147MN2wzMAQ6NS2dQ==::Writer", ptr %2, i32 0, i32 1
store ptr @"0zNZN147MN2wzMAQ6NS2dQ==::File.write", ptr %4
%5 = load %"0zNZN147MN2wzMAQ6NS2dQ==::Writer", ptr %2
call void @"0zNZN147MN2wzMAQ6NS2dQ==::sayHello"(%"0zNZN147MN2wzMAQ6NS2dQ==::Writer" %5)
ret i64 0
}
define void @"0zNZN147MN2wzMAQ6NS2dQ==::sayHello"(%"0zNZN147MN2wzMAQ6NS2dQ==::Writer" %writer) {
0:
%1 = alloca %"0zNZN147MN2wzMAQ6NS2dQ==::Writer"
store %"0zNZN147MN2wzMAQ6NS2dQ==::Writer" %writer, ptr %1
%2 = alloca { ptr, %"AAAAAAAAAAAAAAAAAAAAAA==::Index" }
%3 = alloca [17 x %"AAAAAAAAAAAAAAAAAAAAAA==::Byte"]
%4 = getelementptr [17 x %"AAAAAAAAAAAAAAAAAAAAAA==::Byte"], ptr %3, i32 0, i32 0
store i8 119, ptr %4
%5 = getelementptr [17 x %"AAAAAAAAAAAAAAAAAAAAAA==::Byte"], ptr %3, i32 0, i32 1
store i8 101, ptr %5
%6 = getelementptr [17 x %"AAAAAAAAAAAAAAAAAAAAAA==::Byte"], ptr %3, i32 0, i32 2
store i8 108, ptr %6
%7 = getelementptr [17 x %"AAAAAAAAAAAAAAAAAAAAAA==::Byte"], ptr %3, i32 0, i32 3
store i8 108, ptr %7
%8 = getelementptr [17 x %"AAAAAAAAAAAAAAAAAAAAAA==::Byte"], ptr %3, i32 0, i32 4
store i8 32, ptr %8
%9 = getelementptr [17 x %"AAAAAAAAAAAAAAAAAAAAAA==::Byte"], ptr %3, i32 0, i32 5
store i8 104, ptr %9
%10 = getelementptr [17 x %"AAAAAAAAAAAAAAAAAAAAAA==::Byte"], ptr %3, i32 0, i32 6
store i8 101, ptr %10
%11 = getelementptr [17 x %"AAAAAAAAAAAAAAAAAAAAAA==::Byte"], ptr %3, i32 0, i32 7
store i8 108, ptr %11
%12 = getelementptr [17 x %"AAAAAAAAAAAAAAAAAAAAAA==::Byte"], ptr %3, i32 0, i32 8
store i8 108, ptr %12
%13 = getelementptr [17 x %"AAAAAAAAAAAAAAAAAAAAAA==::Byte"], ptr %3, i32 0, i32 9
store i8 111, ptr %13
%14 = getelementptr [17 x %"AAAAAAAAAAAAAAAAAAAAAA==::Byte"], ptr %3, i32 0, i32 10
store i8 32, ptr %14
%15 = getelementptr [17 x %"AAAAAAAAAAAAAAAAAAAAAA==::Byte"], ptr %3, i32 0, i32 11
store i8 116, ptr %15
%16 = getelementptr [17 x %"AAAAAAAAAAAAAAAAAAAAAA==::Byte"], ptr %3, i32 0, i32 12
store i8 104, ptr %16
%17 = getelementptr [17 x %"AAAAAAAAAAAAAAAAAAAAAA==::Byte"], ptr %3, i32 0, i32 13
store i8 101, ptr %17
%18 = getelementptr [17 x %"AAAAAAAAAAAAAAAAAAAAAA==::Byte"], ptr %3, i32 0, i32 14
store i8 105, ptr %18
%19 = getelementptr [17 x %"AAAAAAAAAAAAAAAAAAAAAA==::Byte"], ptr %3, i32 0, i32 15
store i8 114, ptr %19
%20 = getelementptr [17 x %"AAAAAAAAAAAAAAAAAAAAAA==::Byte"], ptr %3, i32 0, i32 16
store i8 10, ptr %20
%21 = getelementptr { ptr, %"AAAAAAAAAAAAAAAAAAAAAA==::Index" }, ptr %2, i32 0, i32 0
store ptr %3, ptr %21
%22 = getelementptr { ptr, %"AAAAAAAAAAAAAAAAAAAAAA==::Index" }, ptr %2, i32 0, i32 1
store %"AAAAAAAAAAAAAAAAAAAAAA==::Index" 17, ptr %22
%23 = load { ptr, %"AAAAAAAAAAAAAAAAAAAAAA==::Index" }, ptr %2
%24 = getelementptr %"0zNZN147MN2wzMAQ6NS2dQ==::Writer", ptr %1, i32 0, i32 1
%25 = getelementptr %"0zNZN147MN2wzMAQ6NS2dQ==::Writer", ptr %1, i32 0, i32 0
%26 = load ptr, ptr %25
%27 = load ptr, ptr %24
%28 = call %"AAAAAAAAAAAAAAAAAAAAAA==::Index" %27(ptr %26, { ptr, %"AAAAAAAAAAAAAAAAAAAAAA==::Index" } %23)
ret void
}
define %"AAAAAAAAAAAAAAAAAAAAAA==::Index" @"0zNZN147MN2wzMAQ6NS2dQ==::File.write"(ptr %this, { ptr, %"AAAAAAAAAAAAAAAAAAAAAA==::Index" } %buffer) {
0:
%1 = alloca ptr
store ptr %this, ptr %1
%2 = alloca { ptr, %"AAAAAAAAAAAAAAAAAAAAAA==::Index" }
store { ptr, %"AAAAAAAAAAAAAAAAAAAAAA==::Index" } %buffer, ptr %2
%3 = load ptr, ptr %1
%4 = load %"0zNZN147MN2wzMAQ6NS2dQ==::File", ptr %3
%5 = getelementptr { ptr, %"AAAAAAAAAAAAAAAAAAAAAA==::Index" }, ptr %2, i32 0, i32 0
%6 = load ptr, ptr %5
%7 = getelementptr { ptr, %"AAAAAAAAAAAAAAAAAAAAAA==::Index" }, ptr %2, i32 0, i32 1
%8 = load %"AAAAAAAAAAAAAAAAAAAAAA==::Index", ptr %7
%9 = call %"AAAAAAAAAAAAAAAAAAAAAA==::Index" @"0zNZN147MN2wzMAQ6NS2dQ==::write"(%"0zNZN147MN2wzMAQ6NS2dQ==::File" %4, ptr %6, %"AAAAAAAAAAAAAAAAAAAAAA==::Index" %8)
ret %"AAAAAAAAAAAAAAAAAAAAAA==::Index" %9
}
declare %"AAAAAAAAAAAAAAAAAAAAAA==::Index" @"0zNZN147MN2wzMAQ6NS2dQ==::write"(%"0zNZN147MN2wzMAQ6NS2dQ==::File" %fd, ptr %buffer, %"AAAAAAAAAAAAAAAAAAAAAA==::Index" %count)
`,
`
[write fd:File buffer:*Byte count:Index]: Index
Writer: (~ [write buffer:*:Byte]: Index)
File: I32
File.[write buffer:*:Byte]:Index = [write [.this] [~*Byte buffer] [#buffer]]
[sayHello writer:Writer] = writer.[write 'well hello their\n']
[main]:Int 'main' = {
stdout:File = 0
[sayHello stdout]
0
}
`)
}
func TestInterfaceInStruct (test *testing.T) {
testString (test,
`%"0zNZN147MN2wzMAQ6NS2dQ==::Writer" = type { ptr, ptr }
%"0zNZN147MN2wzMAQ6NS2dQ==::A" = type { %"0zNZN147MN2wzMAQ6NS2dQ==::Writer" }
%"0zNZN147MN2wzMAQ6NS2dQ==::File" = type i32
%"AAAAAAAAAAAAAAAAAAAAAA==::Index" = type i64
define void @main() {
0:
%1 = alloca %"0zNZN147MN2wzMAQ6NS2dQ==::A"
%2 = getelementptr %"0zNZN147MN2wzMAQ6NS2dQ==::A", ptr %1, i32 0, i32 0
%3 = getelementptr %"0zNZN147MN2wzMAQ6NS2dQ==::Writer", ptr %2, i32 0, i32 0
%4 = alloca i32
store i32 0, ptr %4
store ptr %4, ptr %3
%5 = getelementptr %"0zNZN147MN2wzMAQ6NS2dQ==::Writer", ptr %2, i32 0, i32 1
store ptr @"0zNZN147MN2wzMAQ6NS2dQ==::File.write", ptr %5
ret void
}
declare %"AAAAAAAAAAAAAAAAAAAAAA==::Index" @"0zNZN147MN2wzMAQ6NS2dQ==::File.write"(ptr %this, { ptr, %"AAAAAAAAAAAAAAAAAAAAAA==::Index" } %buffer)
`,
`
Writer: (~ [write buffer:*:Byte]: Index)
A: (. output: Writer)
File: I32
File.[write buffer:*:Byte]:Index
[main] 'main' = {
a:A = (. output: [~File 0])
}
`)
}
func TestInterfaceAssignment (test *testing.T) {
testString (test,
`%"0zNZN147MN2wzMAQ6NS2dQ==::Face" = type { ptr, ptr }
%"0zNZN147MN2wzMAQ6NS2dQ==::Impl" = type i64
define void @main() {
0:
%1 = alloca %"0zNZN147MN2wzMAQ6NS2dQ==::Face"
%2 = getelementptr %"0zNZN147MN2wzMAQ6NS2dQ==::Face", ptr %1, i32 0, i32 0
%3 = alloca %"0zNZN147MN2wzMAQ6NS2dQ==::Impl"
store ptr %3, ptr %2
%4 = getelementptr %"0zNZN147MN2wzMAQ6NS2dQ==::Face", ptr %1, i32 0, i32 1
store ptr @"0zNZN147MN2wzMAQ6NS2dQ==::Impl.x", ptr %4
ret void
}
define i64 @"0zNZN147MN2wzMAQ6NS2dQ==::Impl.x"(ptr %this) {
0:
%1 = alloca ptr
store ptr %this, ptr %1
ret i64 5
}
`,
`
Face: (~ [x]:Int)
Impl: Int
Impl.[x]:Int = 5
[main] 'main' = {
i:Face = f:Impl
}
`)
}

279
generator/literal.go Normal file
View File

@ -0,0 +1,279 @@
package generator
import "fmt"
import "errors"
import "git.tebibyte.media/fspl/fspl/llvm"
import "git.tebibyte.media/fspl/fspl/entity"
import "git.tebibyte.media/fspl/fspl/analyzer"
func (this *generator) generateLiteralInt (literal *entity.LiteralInt) (llvm.Value, error) {
irType, err := this.generateType(analyzer.ReduceToBase(literal.Type()))
if err != nil { return nil, err }
base := analyzer.ReduceToBase(literal.Type())
switch base.(type) {
case *entity.TypeInt, *entity.TypeWord:
return llvm.NewConstInt(irType.(*llvm.TypeInt), int64(literal.Value)), nil
case *entity.TypeFloat:
return llvm.NewConstFloat(irType.(*llvm.TypeFloat), float64(literal.Value)), nil
default:
return nil, errors.New(fmt.Sprintln("int can't be used as", base))
}
}
func (this *generator) generateLiteralFloat (literal *entity.LiteralFloat) (llvm.Value, error) {
irType, err := this.generateType(analyzer.ReduceToBase(literal.Type()))
if err != nil { return nil, err }
return llvm.NewConstFloat(irType.(*llvm.TypeFloat), literal.Value), nil
}
// generateLiteralArrayLoc generates an array literal. irDestLoc specifies the
// location to assign the data to. If it is nil, this function will allocate a
// destination (if necessary) and return its value as a register.
func (this *generator) generateLiteralArrayLoc (literal *entity.LiteralArray, irDestLoc llvm.Value) (llvm.Value, error) {
destinationSpecified := irDestLoc != nil
makeDataType := func (elementType entity.Type, nullTerminator bool) (llvm.Type, int, error) {
length := len(literal.Elements)
if nullTerminator { length += 1 }
irDataType, err := this.generateType(&entity.TypeArray {
Element: elementType,
Length: length,
})
return irDataType, length, err
}
populateData := func (elementType entity.Type, irDestLoc llvm.Value, length int, nullTerminator bool) error {
irDataType, _, err := makeDataType(elementType, nullTerminator)
if err != nil { return err }
irElementType, err := this.generateType(elementType)
if err != nil { return err }
for index := 0; index < length; index ++ {
elementPointer := this.blockManager.NewGetElementPtr (
irDataType, irDestLoc,
llvm.NewConstInt(llvm.I32, 0),
llvm.NewConstInt(llvm.I32, int64(index)))
if index >= len(literal.Elements) {
this.blockManager.NewStore (
llvm.NewConstZeroInitializer(irElementType),
elementPointer)
} else {
_, err := this.generateAssignmentToDestination (
literal.Elements[index],
elementType,
elementPointer)
if err != nil { return err }
}
}
return nil
}
irDestType, err := this.generateType(literal.Type())
if err != nil { return nil, err }
if !destinationSpecified {
irDestLoc = this.blockManager.newAllocaFront(irDestType)
}
base := analyzer.ReduceToBase(literal.Type())
switch base := base.(type) {
case *entity.TypeArray:
populateData(base.Element, irDestLoc, base.Length, false)
case *entity.TypeSlice:
irDataType, length, err := makeDataType(base.Element, false)
if err != nil { return nil, err }
destDataLoc := this.blockManager.newAllocaFront(irDataType)
err = populateData(base.Element, destDataLoc, length, false)
if err != nil { return nil, err }
this.sliceSetData(literal.Type(), irDestLoc, destDataLoc)
this.sliceSetDefinedLength(literal.Type(), irDestLoc, int64(len(literal.Elements)))
case *entity.TypePointer:
irDataType, length, err := makeDataType(base.Referenced, true)
if err != nil { return nil, err }
destDataLoc := this.blockManager.newAllocaFront(irDataType)
err = populateData(base.Referenced, destDataLoc, length, true)
if err != nil { return nil, err }
this.blockManager.NewStore(destDataLoc, irDestLoc)
default:
return nil, errors.New(fmt.Sprintln("array can't be used as ", base))
}
if destinationSpecified {
return nil, nil
} else {
return irDestLoc, nil
}
}
func (this *generator) generateLiteralStringLoc (literal *entity.LiteralString, irDestLoc llvm.Value) (llvm.Value, error) {
destinationSpecified := irDestLoc != nil
makeDataType := func (anyElementType entity.Type, nullTerminator bool) (llvm.Type, int, error) {
elementType, ok := analyzer.ReduceToBase(anyElementType).(*entity.TypeInt)
if !ok {
return nil, 0, errors.New(fmt.Sprintln (
"string can't be used with element type",
anyElementType))
}
var length int; switch {
case elementType.Width >= 32: length = len(literal.ValueUTF32)
case elementType.Width >= 16: length = len(literal.ValueUTF16)
default: length = len(literal.ValueUTF8)
}
if nullTerminator { length += 1 }
irDataType, err := this.generateType(&entity.TypeArray {
Element: anyElementType,
Length: length,
})
return irDataType, length, err
}
populateData := func (anyElementType entity.Type, irDestLoc llvm.Value, length int, nullTerminator bool) error {
elementType, ok := analyzer.ReduceToBase(anyElementType).(*entity.TypeInt)
if !ok {
return errors.New(fmt.Sprintln (
"string can't be used with element type",
anyElementType))
}
irArrayType, _, err := makeDataType(anyElementType, nullTerminator)
if err != nil { return err }
for index := 0; index < length; index ++ {
var element llvm.Value; switch {
case elementType.Width >= 32:
if index >= len(literal.ValueUTF32) {
element = llvm.NewConstZeroInitializer(llvm.I32)
} else {
element = llvm.NewConstInt(llvm.I32, int64(literal.ValueUTF32[index]))
}
case elementType.Width >= 16:
if index >= len(literal.ValueUTF16) {
element = llvm.NewConstZeroInitializer(llvm.I16)
} else {
element = llvm.NewConstInt(llvm.I16, int64(literal.ValueUTF16[index]))
}
default:
if index >= len(literal.ValueUTF8) {
element = llvm.NewConstZeroInitializer(llvm.I8)
} else {
element = llvm.NewConstInt(llvm.I8, int64(literal.ValueUTF8[index]))
}
}
elementPointer := this.blockManager.NewGetElementPtr (
irArrayType, irDestLoc,
llvm.NewConstInt(llvm.I32, 0),
llvm.NewConstInt(llvm.I32, int64(index)))
this.blockManager.NewStore(element, elementPointer)
}
return nil
}
irDestType, err := this.generateType(literal.Type())
if err != nil { return nil, err }
if !destinationSpecified {
irDestLoc = this.blockManager.newAllocaFront(irDestType)
}
base := analyzer.ReduceToBase(literal.Type())
switch base := base.(type) {
case *entity.TypeInt:
var value llvm.Value; switch {
case base.Width >= 32:
value = llvm.NewConstInt(llvm.I32, int64(literal.ValueUTF32[0]))
case base.Width >= 16:
value = llvm.NewConstInt(llvm.I16, int64(literal.ValueUTF16[0]))
default:
value = llvm.NewConstInt(llvm.I8, int64(literal.ValueUTF8[0]))
}
this.blockManager.NewStore(value, irDestLoc)
case *entity.TypeArray:
err := populateData(base.Element, irDestLoc, base.Length, false)
if err != nil { return nil, err }
case *entity.TypeSlice:
irDataType, length, err := makeDataType(base.Element, false)
if err != nil { return nil, err }
destDataLoc := this.blockManager.newAllocaFront(irDataType)
err = populateData(base.Element, destDataLoc, length, false)
if err != nil { return nil, err }
this.sliceSetData(literal.Type(), irDestLoc, destDataLoc)
this.sliceSetDefinedLength(literal.Type(), irDestLoc, int64(length))
case *entity.TypePointer:
irDataType, length, err := makeDataType(base.Referenced, true)
if err != nil { return nil, err }
destDataLoc := this.blockManager.newAllocaFront(irDataType)
err = populateData(base.Referenced, destDataLoc, length, true)
if err != nil { return nil, err }
this.blockManager.NewStore(destDataLoc, irDestLoc)
default:
return nil, errors.New(fmt.Sprintln("string can't be used as ", base))
}
if destinationSpecified {
return nil, nil
} else {
return irDestLoc, nil
}
}
func (this *generator) generateLiteralStructLoc (literal *entity.LiteralStruct, irDestLoc llvm.Value) (llvm.Value, error) {
destinationSpecified := irDestLoc != nil
base, ok := analyzer.ReduceToBase(literal.Type()).(*entity.TypeStruct)
if !ok { return nil, errors.New(fmt.Sprintln("struct can't be used as ", literal.Type())) }
irDestType, err := this.generateType(literal.Type())
if err != nil { return nil, err }
if !destinationSpecified {
irDestLoc = this.blockManager.newAllocaFront(irDestType)
}
for index, member := range base.Members {
elementPointer := this.blockManager.NewGetElementPtr (
irDestType, irDestLoc,
llvm.NewConstInt(llvm.I32, 0),
llvm.NewConstInt(llvm.I32, int64(index)))
if pair, ok := literal.MemberMap[member.Name]; ok {
_, err = this.generateAssignmentToDestination (
pair.Value,
member.Type(),
elementPointer)
if err != nil { return nil, err }
} else {
irMemberType, err := this.generateType(member.Type())
if err != nil { return nil, err }
this.blockManager.NewStore (
llvm.NewConstZeroInitializer(irMemberType),
elementPointer)
}
}
if destinationSpecified {
return nil, nil
} else {
return irDestLoc, nil
}
}
func (this *generator) generateLiteralBoolean (literal *entity.LiteralBoolean) (llvm.Value, error) {
return llvm.NewConstBool(bool(literal.Value)), nil
}
func (this *generator) generateLiteralNil (literal *entity.LiteralNil) (llvm.Value, error) {
ty, err := this.generateType(literal.Type())
if err != nil { return nil, err }
return llvm.NewConstZeroInitializer(ty), nil
}

372
generator/literal_test.go Normal file
View File

@ -0,0 +1,372 @@
package generator
import "testing"
func TestLiteralInteger (test *testing.T) {
testString (test,
`%"AAAAAAAAAAAAAAAAAAAAAA==::Byte" = type i8
define void @main() {
0:
%1 = alloca i64
store i64 382, ptr %1
%2 = alloca i64
store i64 -4392, ptr %2
%3 = alloca %"AAAAAAAAAAAAAAAAAAAAAA==::Byte"
store i8 128, ptr %3
%4 = alloca i8
store i8 -127, ptr %4
ret void
}
`,
`
[main] 'main' = {
ia:Int = 382
ib:Int = -4392
ic:Byte = 128
id:I8 = -127
}
`)
}
func TestLiteralArray (test *testing.T) {
testString (test,
`%"AAAAAAAAAAAAAAAAAAAAAA==::Index" = type i64
define void @main() {
0:
%1 = alloca [3 x i64]
%2 = getelementptr [3 x i64], ptr %1, i32 0, i32 0
store i64 1, ptr %2
%3 = getelementptr [3 x i64], ptr %1, i32 0, i32 1
store i64 3, ptr %3
%4 = getelementptr [3 x i64], ptr %1, i32 0, i32 2
store i64 5, ptr %4
%5 = alloca { ptr, %"AAAAAAAAAAAAAAAAAAAAAA==::Index" }
%6 = alloca [3 x i64]
%7 = getelementptr [3 x i64], ptr %6, i32 0, i32 0
store i64 4, ptr %7
%8 = getelementptr [3 x i64], ptr %6, i32 0, i32 1
store i64 6, ptr %8
%9 = getelementptr [3 x i64], ptr %6, i32 0, i32 2
store i64 8, ptr %9
%10 = getelementptr { ptr, %"AAAAAAAAAAAAAAAAAAAAAA==::Index" }, ptr %5, i32 0, i32 0
store ptr %6, ptr %10
%11 = getelementptr { ptr, %"AAAAAAAAAAAAAAAAAAAAAA==::Index" }, ptr %5, i32 0, i32 1
store %"AAAAAAAAAAAAAAAAAAAAAA==::Index" 3, ptr %11
%12 = alloca ptr
%13 = alloca [4 x i64]
%14 = getelementptr [4 x i64], ptr %13, i32 0, i32 0
store i64 7, ptr %14
%15 = getelementptr [4 x i64], ptr %13, i32 0, i32 1
store i64 9, ptr %15
%16 = getelementptr [4 x i64], ptr %13, i32 0, i32 2
store i64 11, ptr %16
%17 = getelementptr [4 x i64], ptr %13, i32 0, i32 3
store i64 zeroinitializer, ptr %17
store ptr %13, ptr %12
ret void
}
`,
`
[main] 'main' = {
a:3:Int = (* 1 3 5)
b:*:Int = (* 4 6 8)
c:*Int = (* 7 9 11)
}
`)
}
func TestLiteralArrayNested (test *testing.T) {
testString (test,
`%"0zNZN147MN2wzMAQ6NS2dQ==::A" = type [2 x [2 x i64]]
define void @main() {
0:
%1 = alloca %"0zNZN147MN2wzMAQ6NS2dQ==::A"
%2 = getelementptr [2 x [2 x i64]], ptr %1, i32 0, i32 0
%3 = getelementptr [2 x i64], ptr %2, i32 0, i32 0
store i64 70, ptr %3
%4 = getelementptr [2 x i64], ptr %2, i32 0, i32 1
store i64 90, ptr %4
%5 = getelementptr [2 x [2 x i64]], ptr %1, i32 0, i32 1
%6 = getelementptr [2 x i64], ptr %5, i32 0, i32 0
store i64 2, ptr %6
%7 = getelementptr [2 x i64], ptr %5, i32 0, i32 1
store i64 4, ptr %7
ret void
}
`,
`
A: 2:2:Int
[main] 'main' = {
a:A = (*
(* 70 90)
(* 2 4))
}
`)
}
func TestLiteralArrayUndersized (test *testing.T) {
testString (test,
`define void @main() {
0:
%1 = alloca [4 x i64]
%2 = getelementptr [3 x i64], ptr %1, i32 0, i32 0
store i64 5, ptr %2
%3 = getelementptr [3 x i64], ptr %1, i32 0, i32 1
store i64 9, ptr %3
%4 = getelementptr [3 x i64], ptr %1, i32 0, i32 2
store i64 3, ptr %4
%5 = getelementptr [3 x i64], ptr %1, i32 0, i32 3
store i64 zeroinitializer, ptr %5
ret void
}
`,
`
[main] 'main' = {
a:4:Int = (* 5 9 3)
}
`)
}
func TestLiteralString (test *testing.T) {
testString (test,
`%"AAAAAAAAAAAAAAAAAAAAAA==::Byte" = type i8
%"AAAAAAAAAAAAAAAAAAAAAA==::Rune" = type i32
%"AAAAAAAAAAAAAAAAAAAAAA==::Index" = type i64
%"AAAAAAAAAAAAAAAAAAAAAA==::String" = type { ptr, %"AAAAAAAAAAAAAAAAAAAAAA==::Index" }
define void @main() {
0:
%1 = alloca %"AAAAAAAAAAAAAAAAAAAAAA==::Byte"
%2 = alloca %"AAAAAAAAAAAAAAAAAAAAAA==::Byte"
store i8 97, ptr %2
%3 = load %"AAAAAAAAAAAAAAAAAAAAAA==::Byte", ptr %2
store %"AAAAAAAAAAAAAAAAAAAAAA==::Byte" %3, ptr %1
%4 = alloca %"AAAAAAAAAAAAAAAAAAAAAA==::Rune"
%5 = alloca %"AAAAAAAAAAAAAAAAAAAAAA==::Rune"
store i32 241, ptr %5
%6 = load %"AAAAAAAAAAAAAAAAAAAAAA==::Rune", ptr %5
store %"AAAAAAAAAAAAAAAAAAAAAA==::Rune" %6, ptr %4
%7 = alloca [5 x %"AAAAAAAAAAAAAAAAAAAAAA==::Byte"]
%8 = getelementptr [2 x %"AAAAAAAAAAAAAAAAAAAAAA==::Byte"], ptr %7, i32 0, i32 0
store i8 115, ptr %8
%9 = getelementptr [2 x %"AAAAAAAAAAAAAAAAAAAAAA==::Byte"], ptr %7, i32 0, i32 1
store i8 99, ptr %9
%10 = getelementptr [2 x %"AAAAAAAAAAAAAAAAAAAAAA==::Byte"], ptr %7, i32 0, i32 2
store i8 zeroinitializer, ptr %10
%11 = getelementptr [2 x %"AAAAAAAAAAAAAAAAAAAAAA==::Byte"], ptr %7, i32 0, i32 3
store i8 zeroinitializer, ptr %11
%12 = getelementptr [2 x %"AAAAAAAAAAAAAAAAAAAAAA==::Byte"], ptr %7, i32 0, i32 4
store i8 zeroinitializer, ptr %12
%13 = alloca [5 x %"AAAAAAAAAAAAAAAAAAAAAA==::Rune"]
%14 = getelementptr [2 x %"AAAAAAAAAAAAAAAAAAAAAA==::Rune"], ptr %13, i32 0, i32 0
store i32 115, ptr %14
%15 = getelementptr [2 x %"AAAAAAAAAAAAAAAAAAAAAA==::Rune"], ptr %13, i32 0, i32 1
store i32 100, ptr %15
%16 = getelementptr [2 x %"AAAAAAAAAAAAAAAAAAAAAA==::Rune"], ptr %13, i32 0, i32 2
store i32 zeroinitializer, ptr %16
%17 = getelementptr [2 x %"AAAAAAAAAAAAAAAAAAAAAA==::Rune"], ptr %13, i32 0, i32 3
store i32 zeroinitializer, ptr %17
%18 = getelementptr [2 x %"AAAAAAAAAAAAAAAAAAAAAA==::Rune"], ptr %13, i32 0, i32 4
store i32 zeroinitializer, ptr %18
%19 = alloca %"AAAAAAAAAAAAAAAAAAAAAA==::String"
%20 = alloca [2 x i8]
%21 = getelementptr [2 x i8], ptr %20, i32 0, i32 0
store i8 115, ptr %21
%22 = getelementptr [2 x i8], ptr %20, i32 0, i32 1
store i8 101, ptr %22
%23 = getelementptr %"AAAAAAAAAAAAAAAAAAAAAA==::String", ptr %19, i32 0, i32 0
store ptr %20, ptr %23
%24 = getelementptr %"AAAAAAAAAAAAAAAAAAAAAA==::String", ptr %19, i32 0, i32 1
store %"AAAAAAAAAAAAAAAAAAAAAA==::Index" 2, ptr %24
ret void
}
`,
`
[main] 'main' = {
sa:Byte = 'a'
sb:Rune = 'ñ'
sc:5:Byte = 'sc'
sd:5:Rune = 'sd'
se:String = 'se'
}
`)
}
func TestLiteralStringPointer (test *testing.T) {
testString (test,
`%"AAAAAAAAAAAAAAAAAAAAAA==::Byte" = type i8
define void @main() {
0:
%1 = alloca ptr
%2 = alloca [3 x %"AAAAAAAAAAAAAAAAAAAAAA==::Byte"]
%3 = getelementptr [3 x %"AAAAAAAAAAAAAAAAAAAAAA==::Byte"], ptr %2, i32 0, i32 0
store i8 115, ptr %3
%4 = getelementptr [3 x %"AAAAAAAAAAAAAAAAAAAAAA==::Byte"], ptr %2, i32 0, i32 1
store i8 102, ptr %4
%5 = getelementptr [3 x %"AAAAAAAAAAAAAAAAAAAAAA==::Byte"], ptr %2, i32 0, i32 2
store i8 zeroinitializer, ptr %5
store ptr %2, ptr %1
ret void
}
`,
`
[main] 'main' = {
sf:*Byte = 'sf'
}
`)
}
func TestLiteralStringSlice (test *testing.T) {
testString (test,
`%"AAAAAAAAAAAAAAAAAAAAAA==::Index" = type i64
%"AAAAAAAAAAAAAAAAAAAAAA==::String" = type { ptr, %"AAAAAAAAAAAAAAAAAAAAAA==::Index" }
define void @main() {
0:
%1 = alloca %"AAAAAAAAAAAAAAAAAAAAAA==::String"
%2 = alloca [2 x i8]
%3 = getelementptr [2 x i8], ptr %2, i32 0, i32 0
store i8 115, ptr %3
%4 = getelementptr [2 x i8], ptr %2, i32 0, i32 1
store i8 102, ptr %4
%5 = getelementptr %"AAAAAAAAAAAAAAAAAAAAAA==::String", ptr %1, i32 0, i32 0
store ptr %2, ptr %5
%6 = getelementptr %"AAAAAAAAAAAAAAAAAAAAAA==::String", ptr %1, i32 0, i32 1
store %"AAAAAAAAAAAAAAAAAAAAAA==::Index" 2, ptr %6
ret void
}
`,
`
[main] 'main' = {
sf:String = 'sf'
}
`)
}
func TestLiteralStringUndersized (test *testing.T) {
testString (test,
`%"AAAAAAAAAAAAAAAAAAAAAA==::Byte" = type i8
define void @main() {
0:
%1 = alloca [5 x %"AAAAAAAAAAAAAAAAAAAAAA==::Byte"]
%2 = getelementptr [3 x %"AAAAAAAAAAAAAAAAAAAAAA==::Byte"], ptr %1, i32 0, i32 0
store i8 120, ptr %2
%3 = getelementptr [3 x %"AAAAAAAAAAAAAAAAAAAAAA==::Byte"], ptr %1, i32 0, i32 1
store i8 121, ptr %3
%4 = getelementptr [3 x %"AAAAAAAAAAAAAAAAAAAAAA==::Byte"], ptr %1, i32 0, i32 2
store i8 122, ptr %4
%5 = getelementptr [3 x %"AAAAAAAAAAAAAAAAAAAAAA==::Byte"], ptr %1, i32 0, i32 3
store i8 zeroinitializer, ptr %5
%6 = getelementptr [3 x %"AAAAAAAAAAAAAAAAAAAAAA==::Byte"], ptr %1, i32 0, i32 4
store i8 zeroinitializer, ptr %6
ret void
}
`,
`
[main] 'main' = {
a:5:Byte = 'xyz'
}
`)
}
func TestLiteralStruct (test *testing.T) {
testString (test,
`define void @main() {
0:
%1 = alloca { i64, { i64, i64 } }
%2 = getelementptr { i64, { i64, i64 } }, ptr %1, i32 0, i32 0
store i64 1, ptr %2
%3 = getelementptr { i64, { i64, i64 } }, ptr %1, i32 0, i32 1
%4 = getelementptr { i64, i64 }, ptr %3, i32 0, i32 0
store i64 2, ptr %4
%5 = getelementptr { i64, i64 }, ptr %3, i32 0, i32 1
store i64 3, ptr %5
ret void
}
`,
`
[main] 'main' = {
ta:(.x:Int y:(.z:Int a:Int)) = (.x: 1 y: (.z: 2 a: 3))
}
`)
}
func TestTypedStructInstantiation (test *testing.T) {
testString (test,
`%"0zNZN147MN2wzMAQ6NS2dQ==::A" = type { i64 }
define void @main() {
0:
%1 = alloca %"0zNZN147MN2wzMAQ6NS2dQ==::A"
%2 = getelementptr %"0zNZN147MN2wzMAQ6NS2dQ==::A", ptr %1, i32 0, i32 0
store i64 5, ptr %2
ret void
}
`,
`
A: (.x:Int)
[main] 'main' = {
a:A = (.x: 5)
}
`)
}
func TestTypedArrayInstantiation (test *testing.T) {
testString (test,
`%"0zNZN147MN2wzMAQ6NS2dQ==::A" = type [2 x i64]
define void @main() {
0:
%1 = alloca %"0zNZN147MN2wzMAQ6NS2dQ==::A"
%2 = getelementptr [2 x i64], ptr %1, i32 0, i32 0
store i64 1, ptr %2
%3 = getelementptr [2 x i64], ptr %1, i32 0, i32 1
store i64 2, ptr %3
ret void
}
`,
`
A: 2:Int
[main] 'main' = {
a:A = (* 1 2)
}
`)
}
func TestTypedStringInstantiation (test *testing.T) {
testString (test,
`%"AAAAAAAAAAAAAAAAAAAAAA==::Byte" = type i8
%"0zNZN147MN2wzMAQ6NS2dQ==::A" = type [1 x %"AAAAAAAAAAAAAAAAAAAAAA==::Byte"]
%"AAAAAAAAAAAAAAAAAAAAAA==::Index" = type i64
%"0zNZN147MN2wzMAQ6NS2dQ==::B" = type { ptr, %"AAAAAAAAAAAAAAAAAAAAAA==::Index" }
%"0zNZN147MN2wzMAQ6NS2dQ==::C" = type ptr
define void @main() {
0:
%1 = alloca %"0zNZN147MN2wzMAQ6NS2dQ==::A"
%2 = getelementptr [1 x %"AAAAAAAAAAAAAAAAAAAAAA==::Byte"], ptr %1, i32 0, i32 0
store i8 104, ptr %2
%3 = alloca %"0zNZN147MN2wzMAQ6NS2dQ==::B"
%4 = alloca [1 x %"AAAAAAAAAAAAAAAAAAAAAA==::Byte"]
%5 = getelementptr [1 x %"AAAAAAAAAAAAAAAAAAAAAA==::Byte"], ptr %4, i32 0, i32 0
store i8 104, ptr %5
%6 = getelementptr %"0zNZN147MN2wzMAQ6NS2dQ==::B", ptr %3, i32 0, i32 0
store ptr %4, ptr %6
%7 = getelementptr %"0zNZN147MN2wzMAQ6NS2dQ==::B", ptr %3, i32 0, i32 1
store %"AAAAAAAAAAAAAAAAAAAAAA==::Index" 1, ptr %7
%8 = alloca %"0zNZN147MN2wzMAQ6NS2dQ==::C"
%9 = alloca [2 x %"AAAAAAAAAAAAAAAAAAAAAA==::Byte"]
%10 = getelementptr [2 x %"AAAAAAAAAAAAAAAAAAAAAA==::Byte"], ptr %9, i32 0, i32 0
store i8 104, ptr %10
%11 = getelementptr [2 x %"AAAAAAAAAAAAAAAAAAAAAA==::Byte"], ptr %9, i32 0, i32 1
store i8 zeroinitializer, ptr %11
store ptr %9, ptr %8
ret void
}
`,
`
A: 1:Byte
B: *:Byte
C: *Byte
[main] 'main' = {
a:A = 'h'
b:B = 'h'
c:C = 'h'
}
`)
}

178
generator/misc_test.go Normal file
View File

@ -0,0 +1,178 @@
package generator
import "testing"
func TestPrintDigit (test *testing.T) {
testString (test,
`%"AAAAAAAAAAAAAAAAAAAAAA==::Byte" = type i8
%"AAAAAAAAAAAAAAAAAAAAAA==::Index" = type i64
define void @"0zNZN147MN2wzMAQ6NS2dQ==::printDigit"(%"AAAAAAAAAAAAAAAAAAAAAA==::Byte" %digit) {
0:
%1 = alloca %"AAAAAAAAAAAAAAAAAAAAAA==::Byte"
store %"AAAAAAAAAAAAAAAAAAAAAA==::Byte" %digit, ptr %1
%2 = load %"AAAAAAAAAAAAAAAAAAAAAA==::Byte", ptr %1
%3 = alloca %"AAAAAAAAAAAAAAAAAAAAAA==::Byte"
store i8 48, ptr %3
%4 = load %"AAAAAAAAAAAAAAAAAAAAAA==::Byte", ptr %3
%5 = add %"AAAAAAAAAAAAAAAAAAAAAA==::Byte" %2, %4
store %"AAAAAAAAAAAAAAAAAAAAAA==::Byte" %5, ptr %1
%6 = call %"AAAAAAAAAAAAAAAAAAAAAA==::Index" @write(i32 1, ptr %1, i64 1)
ret void
}
declare %"AAAAAAAAAAAAAAAAAAAAAA==::Index" @write(i32 %file, ptr %buffer, %"AAAAAAAAAAAAAAAAAAAAAA==::Index" %count)
`,
`
[write file:I32 buffer:*Byte count:Index]: Index 'write'
[printDigit digit:Byte] = {
digit = [+ digit '0']
[write 1 [@digit] 1]
}
`)
}
func TestSignedUnsignedDivision (test *testing.T) {
testString (test,
`define void @"0zNZN147MN2wzMAQ6NS2dQ==::main"() {
0:
%1 = alloca i64
%2 = sdiv i64 20, -5
store i64 %2, ptr %1
%3 = alloca i64
%4 = sdiv i64 15, 3
store i64 %4, ptr %3
%5 = alloca i64
%6 = srem i64 7, 3
store i64 %6, ptr %5
%7 = alloca i64
%8 = udiv i64 20, 5
store i64 %8, ptr %7
%9 = alloca i64
%10 = udiv i64 15, 3
store i64 %10, ptr %9
%11 = alloca i64
%12 = urem i64 7, 3
store i64 %12, ptr %11
ret void
}
`,
`
[main] = {
a:Int = [/ 20 -5]
b:Int = [/ 15 3]
c:Int = [% 7 3]
d:UInt = [/ 20 5]
e:UInt = [/ 15 3]
f:UInt = [% 7 3]
}
`)
}
func TestFloatMath (test *testing.T) {
testString (test,
`define void @"0zNZN147MN2wzMAQ6NS2dQ==::main"() {
0:
%1 = alloca double
%2 = fadd double 2.0, 3.0
store double %2, ptr %1
%3 = alloca double
%4 = load double, ptr %1
%5 = fmul double %4, 100.0
store double %5, ptr %3
ret void
}
`,
`
[main] = {
sum:F64 = [+ 2 3]
prd:F64 = [* sum 100]
}
`)
}
func TestCompare (test *testing.T) {
testString (test,
`%"AAAAAAAAAAAAAAAAAAAAAA==::Bool" = type i8
%"0zNZN147MN2wzMAQ6NS2dQ==::A" = type i64
define void @"0zNZN147MN2wzMAQ6NS2dQ==::main"() {
0:
%1 = alloca i64
store i64 32, ptr %1
%2 = alloca %"AAAAAAAAAAAAAAAAAAAAAA==::Bool"
%3 = load i64, ptr %1
%4 = icmp sgt i64 %3, 1
store i1 %4, ptr %2
%5 = load i64, ptr %1
%6 = icmp slt i64 0, %5
%7 = icmp slt i1 %6, 50
store i1 %7, ptr %2
%8 = load i64, ptr %1
%9 = icmp eq i64 32, %8
store i1 %9, ptr %2
%10 = icmp sgt i64 5, 3
store i1 %10, ptr %2
ret void
}
`,
`
A: Int
[main] = {
x:Int = 32
b:Bool = [> x 1]
b = [< 0 x 50]
b = [= 32 x]
b = [> [~A 5] [~A 3]]
}
`)
}
func TestSlice (test *testing.T) {
testString (test,
`%"AAAAAAAAAAAAAAAAAAAAAA==::Index" = type i64
%"AAAAAAAAAAAAAAAAAAAAAA==::String" = type { ptr, %"AAAAAAAAAAAAAAAAAAAAAA==::Index" }
define void @"0zNZN147MN2wzMAQ6NS2dQ==::print"(%"AAAAAAAAAAAAAAAAAAAAAA==::String" %string) {
0:
%1 = alloca %"AAAAAAAAAAAAAAAAAAAAAA==::String"
store %"AAAAAAAAAAAAAAAAAAAAAA==::String" %string, ptr %1
%2 = alloca %"AAAAAAAAAAAAAAAAAAAAAA==::String"
br label %3
3:
%4 = getelementptr { ptr, %"AAAAAAAAAAAAAAAAAAAAAA==::Index" }, ptr %1, i32 0, i32 1
%5 = load %"AAAAAAAAAAAAAAAAAAAAAA==::Index", ptr %4
%6 = icmp ult %"AAAAAAAAAAAAAAAAAAAAAA==::Index" %5, 1
br i1 %6, label %7, label %8
7:
br label %21
8:
%9 = getelementptr { ptr, %"AAAAAAAAAAAAAAAAAAAAAA==::Index" }, ptr %1, i32 0, i32 0
%10 = load ptr, ptr %9
%11 = call %"AAAAAAAAAAAAAAAAAAAAAA==::Index" @write(i32 1, ptr %10, i64 1)
%12 = getelementptr { ptr, %"AAAAAAAAAAAAAAAAAAAAAA==::Index" }, ptr %1, i32 0, i32 0
%13 = load ptr, ptr %12
%14 = getelementptr { ptr, %"AAAAAAAAAAAAAAAAAAAAAA==::Index" }, ptr %1, i32 0, i32 1
%15 = load %"AAAAAAAAAAAAAAAAAAAAAA==::Index", ptr %14
%16 = sub %"AAAAAAAAAAAAAAAAAAAAAA==::Index" %15, 1
%17 = getelementptr i8, ptr %13, i64 1
%18 = getelementptr %"AAAAAAAAAAAAAAAAAAAAAA==::String", ptr %2, i32 0, i32 0
store ptr %17, ptr %18
%19 = getelementptr %"AAAAAAAAAAAAAAAAAAAAAA==::String", ptr %2, i32 0, i32 1
store %"AAAAAAAAAAAAAAAAAAAAAA==::Index" %16, ptr %19
%20 = load %"AAAAAAAAAAAAAAAAAAAAAA==::String", ptr %2
store %"AAAAAAAAAAAAAAAAAAAAAA==::String" %20, ptr %1
br label %3
21:
ret void
}
declare %"AAAAAAAAAAAAAAAAAAAAAA==::Index" @write(i32 %file, ptr %buffer, %"AAAAAAAAAAAAAAAAAAAAAA==::Index" %count)
`,
`
[write file:I32 buffer:*Byte count:Index]: Index 'write'
[print string: String] = loop {
if [< [#string] 1] then [break]
[write 1 [~*Byte string] 1]
string = [\string 1/]
}
`)
}

125
generator/multiunit_test.go Normal file
View File

@ -0,0 +1,125 @@
package generator
import "testing"
func TestUnitTwo (test *testing.T) {
testUnits (test,
`%"E80Pxr3rNdulEDOmHs9Hsg==::X" = type i64
define %"E80Pxr3rNdulEDOmHs9Hsg==::X" @"0zNZN147MN2wzMAQ6NS2dQ==::main"() {
0:
ret i64 5
}
`,
`[main]:something::X = 5`,
"something.fspl",
`+ X:Int`,
)}
func TestUnitAssignRestricted (test *testing.T) {
testUnits (test,
`%"s0sfKEEPN2W0H9cNKD+OOg==::RestrictedInt" = type i64
define void @"0zNZN147MN2wzMAQ6NS2dQ==::main"() {
0:
%1 = alloca %"s0sfKEEPN2W0H9cNKD+OOg==::RestrictedInt"
%2 = alloca %"s0sfKEEPN2W0H9cNKD+OOg==::RestrictedInt"
%3 = load %"s0sfKEEPN2W0H9cNKD+OOg==::RestrictedInt", ptr %2
store %"s0sfKEEPN2W0H9cNKD+OOg==::RestrictedInt" %3, ptr %1
ret void
}
`,
`[main] = {
x:other::RestrictedInt
y:other::RestrictedInt
x = y
}`,
"other.fspl",
`~ RestrictedInt:Int`,
)}
func TestNestedUnitTypedef (test *testing.T) {
testUnits (test,
`%"kOsrvvRIOh2nYGTmlfGyKQ==::X" = type i64
%"qnTuCId6O/ihTdVz5Ln6WQ==::X" = type %"kOsrvvRIOh2nYGTmlfGyKQ==::X"
define %"qnTuCId6O/ihTdVz5Ln6WQ==::X" @"0zNZN147MN2wzMAQ6NS2dQ==::main"() {
0:
ret i64 5
}
`,
`[main]:layer1::X = 5`,
"layer0.fspl",
`+ X:Int`,
"layer1.fspl",
`+ X:layer0::X`,
)}
func TestUnitInterfaceSatisfaction (test *testing.T) {
testUnits (test,
`%"AAAAAAAAAAAAAAAAAAAAAA==::Index" = type i64
%"qsUY87+PPGG/YPYMcnGU/g==::FileDescriptor" = type i64
%"1cqXRNyHN06gi9QQb4GCsg==::File" = type %"qsUY87+PPGG/YPYMcnGU/g==::FileDescriptor"
%"1cqXRNyHN06gi9QQb4GCsg==::Writer" = type { ptr, ptr }
%"AAAAAAAAAAAAAAAAAAAAAA==::Byte" = type i8
declare %"AAAAAAAAAAAAAAAAAAAAAA==::Index" @write(%"qsUY87+PPGG/YPYMcnGU/g==::FileDescriptor" %file, ptr %buffer, %"AAAAAAAAAAAAAAAAAAAAAA==::Index" %count)
define i32 @main() {
0:
%1 = alloca %"1cqXRNyHN06gi9QQb4GCsg==::File"
store i64 1, ptr %1
%2 = alloca %"1cqXRNyHN06gi9QQb4GCsg==::Writer"
%3 = getelementptr %"1cqXRNyHN06gi9QQb4GCsg==::Writer", ptr %2, i32 0, i32 0
store ptr %1, ptr %3
%4 = getelementptr %"1cqXRNyHN06gi9QQb4GCsg==::Writer", ptr %2, i32 0, i32 1
store ptr @"1cqXRNyHN06gi9QQb4GCsg==::File.write", ptr %4
%5 = load %"1cqXRNyHN06gi9QQb4GCsg==::Writer", ptr %2
call void @"0zNZN147MN2wzMAQ6NS2dQ==::sayHello"(%"1cqXRNyHN06gi9QQb4GCsg==::Writer" %5)
ret i32 0
}
define void @"0zNZN147MN2wzMAQ6NS2dQ==::sayHello"(%"1cqXRNyHN06gi9QQb4GCsg==::Writer" %writer) {
0:
%1 = alloca %"1cqXRNyHN06gi9QQb4GCsg==::Writer"
store %"1cqXRNyHN06gi9QQb4GCsg==::Writer" %writer, ptr %1
%2 = alloca { ptr, %"AAAAAAAAAAAAAAAAAAAAAA==::Index" }
%3 = alloca [3 x %"AAAAAAAAAAAAAAAAAAAAAA==::Byte"]
%4 = getelementptr [3 x %"AAAAAAAAAAAAAAAAAAAAAA==::Byte"], ptr %3, i32 0, i32 0
store i8 104, ptr %4
%5 = getelementptr [3 x %"AAAAAAAAAAAAAAAAAAAAAA==::Byte"], ptr %3, i32 0, i32 1
store i8 105, ptr %5
%6 = getelementptr [3 x %"AAAAAAAAAAAAAAAAAAAAAA==::Byte"], ptr %3, i32 0, i32 2
store i8 10, ptr %6
%7 = getelementptr { ptr, %"AAAAAAAAAAAAAAAAAAAAAA==::Index" }, ptr %2, i32 0, i32 0
store ptr %3, ptr %7
%8 = getelementptr { ptr, %"AAAAAAAAAAAAAAAAAAAAAA==::Index" }, ptr %2, i32 0, i32 1
store %"AAAAAAAAAAAAAAAAAAAAAA==::Index" 3, ptr %8
%9 = load { ptr, %"AAAAAAAAAAAAAAAAAAAAAA==::Index" }, ptr %2
%10 = getelementptr %"1cqXRNyHN06gi9QQb4GCsg==::Writer", ptr %1, i32 0, i32 1
%11 = getelementptr %"1cqXRNyHN06gi9QQb4GCsg==::Writer", ptr %1, i32 0, i32 0
%12 = load ptr, ptr %11
%13 = load ptr, ptr %10
%14 = call %"AAAAAAAAAAAAAAAAAAAAAA==::Index" %13(ptr %12, { ptr, %"AAAAAAAAAAAAAAAAAAAAAA==::Index" } %9)
ret void
}
declare %"AAAAAAAAAAAAAAAAAAAAAA==::Index" @"1cqXRNyHN06gi9QQb4GCsg==::File.write"(ptr %this, { ptr, %"AAAAAAAAAAAAAAAAAAAAAA==::Index" } %buffer)
`,
`[sayHello writer:io::Writer] = writer.[write 'hi\n']
[main]: I32 'main' = {
stdout:io::File = 1
[sayHello stdout]
0
}`,
"cstdio.fspl",
`+ FileDescriptor: Int
+ [write file:FileDescriptor buffer:*Byte count:Index]: Index 'write'`,
"io.fspl",
`+ Writer: (~ [write buffer:*:Byte]: Index)
+ File: cstdio::FileDescriptor
+ File.[write buffer:*:Byte]:Index =
cstdio::[write
[~cstdio::FileDescriptor [.this]]
[~*Byte buffer] [#buffer]]`,
)}

View File

@ -0,0 +1,10 @@
// Package native provides a generator target describing the current system.
// This is accomplished using several conditionally compiled source files.
package native
import "git.tebibyte.media/fspl/fspl/generator"
// NativeTarget returns a target describing the current system.
func NativeTarget () generator.Target {
return nativeTarget()
}

View File

@ -0,0 +1,10 @@
package native
import "git.tebibyte.media/fspl/fspl/generator"
func nativeTarget () generator.Target {
return generator.Target {
WordSize: 32,
Arch: "i386",
}
}

View File

@ -0,0 +1,10 @@
package native
import "git.tebibyte.media/fspl/fspl/generator"
func nativeTarget () generator.Target {
return generator.Target {
WordSize: 64,
Arch: "x86_64",
}
}

View File

@ -0,0 +1,11 @@
package native
import "git.tebibyte.media/fspl/fspl/generator"
func nativeTarget () generator.Target {
// this may not be accurate, can't find info online about amd64p32
return generator.Target {
WordSize: 32,
Arch: "x86",
}
}

View File

@ -0,0 +1,10 @@
package native
import "git.tebibyte.media/fspl/fspl/generator"
func nativeTarget () generator.Target {
return generator.Target {
WordSize: 32,
Arch: "arm",
}
}

View File

@ -0,0 +1,10 @@
package native
import "git.tebibyte.media/fspl/fspl/generator"
func nativeTarget () generator.Target {
return generator.Target {
WordSize: 64,
Arch: "aarch64",
}
}

View File

@ -0,0 +1,10 @@
package native
import "git.tebibyte.media/fspl/fspl/generator"
func nativeTarget () generator.Target {
return generator.Target {
WordSize: 64,
Arch: "aarch64_be",
}
}

Some files were not shown because too many files have changed in this diff Show More