• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    迪恩网络公众号

wiz-lang/wiz: A high-level assembly language for writing homebrew software and g ...

原作者: [db:作者] 来自: 网络 收藏 邀请

开源软件名称:

wiz-lang/wiz

开源软件地址:

https://github.com/wiz-lang/wiz

开源编程语言:

C++ 94.7%

开源软件介绍:

wiz

by Andrew G. Crowell

Wiz is a high-level assembly language for writing homebrew software for retro console platforms.

Contact

Features

Wiz is intended to cross-compile programs that run on specific hardware, as opposed to an abstract machine which requires a runtime library to support it. This means programs must be written with the feature set and limitations of the system being targeted in mind (registers, addressing modes, limitations on instructions), and programs are highly platform-dependent.

Here are some features that Wiz supports:

  • Familiar high-level syntax and structured programming features, such as if, while, for, do/while, functions, etc.
  • Static type system that supports integers, structures, arrays, pointers, and other types. Supports automatic type inference for initializers.
  • Expressions and statements that map directly to low-level instructions.
  • Raw access to the hardware. Low-level register and memory access, branching, stack manipulation, comparison, bit-twiddling, I/O, etc.
  • Compile-time attributes, inlining, constant expression folding, conditional compilation, array comprehensions.
  • Support for multiple different systems and output file formats.
  • bank declarations to customize memory map and output.
  • const and writeonly modifiers to prevent undesired access to memory.

Wiz supports several different CPU architectures, with hopes to support more platforms in the future!

  • 6502 - MOS 6502 (used by the NES, C64, Atari 2600/5200/7800/8-bit, Apple II)
  • 65c02 - MOS 65C02
  • wdc65c02 - WDC 65C02
  • rockwell65c02 - Rockwell 65C02
  • huc6280 - HuC6280 (used by the PC Engine / TurboGrafx-16)
  • wdc65816 - WDC 65816 (used by the SNES, SA-1, Apple IIgs)
  • spc700 - SPC700 (used by the SNES APU)
  • z80 - Zilog Z80 (used by the Sega Master System, Game Gear, MSX, ZX, etc)
  • gb - Game Boy CPU / DMG-CPU / GBZ80 / SM83 / LR35902 (used by the Game Boy and Game Boy Color)

Wiz has built-in support for many ROM formats, and provides conveniences for configuring headers, and for automatically filling in checksums and rounding up ROM sizes. In some case the CPU architecture can be automatically detected by the output format. It currently exports the following output formats:

  • .bin (raw binary format -- supports optional trimming)
  • .nes (NES / Famicom - header config, auto-pad)
  • .gb, .gbc (Game Boy - header config, auto-checksum, auto-pad)
  • .sms, .gg (Sega Master System - header config, auto-checksum, auto-pad)
  • .sfc, .smc (SNES / Super Famicom - header config, auto-checksum, auto-pad)
  • .pce (TurboGrafx-16 / PC Engine)
  • .a26 (Atari 2600)

Using Wiz

Usage:

wiz [options] <input>

Arguments:

  • input - the name of the input file to load. A filename of - implies the standard input stream.
  • -o filename or --output=filename - the name of the output file to produce. the file extension determines the output format, and can sometimes automatically suggest a target system.
  • -m sys or --system=sys - specifies the target system that the program is being built for. Supported systems: 6502, 65c02 rockwell65c02, wdc65c02, huc6280, z80, gb, wdc65816, spc700
  • -I dir or --import-dir=dir - adds a directory to search for import and embed statements.
  • --color=setting - sets the color preference for the terminal (Defaults to auto). auto will automatically detects if a TTY is attached, and only emits color escapes when there is one. none disables color. ansi will always use ANSI-escapes, even if no TTY is detected, or if the terminal uses different method of coloring (eg. Windows console).
  • --help - lists a help message.
  • --version - lists the current compiler version.

Example invocation:

wiz hello.wiz --system=6502 -o hello.nes

Getting Started

For now, please consult the example folder to see how Wiz can be used, or failing that, the compiler source code. Additional documentation isn't ready yet. In the future, there are plans to have a language reference, a reference for the instruction capabilities of each target, and tutorials showing how to make stuff. Help here is greatly appreciated!

For help with editing Wiz files:

  • In VS Code install the extension "Wiz Language Support" from the extension marketplace.
  • In Sublime Text the file syntax/sublime2/wiz.tmLanguage (in this repo) goes in your packages folder which you can open from Sublime Text by going to Preferences > Browse Packages.

Building Source

NOTE: All build instructions require a C++14, C++17, or later compiler. It will not build with an earlier version of the standard.

Windows (Visual Studio)

  • Install Visual Studio 2019 or later.
  • Open vc/wiz.sln in Visual Studio. Build the solution.
  • If the build succeeds, a file named wiz.exe should exist in the bin/ folder under the root of this repository.

Windows (mingw)

  • Install MinGW-w64. https://mingw-w64.org/
  • Install GnuWin32 "Make for Windows". http://gnuwin32.sourceforge.net/packages/make.htm
  • Open a terminal window, such as the Command Prompt (cmd) or something else.
  • Run make in the terminal. For a debug target, run make CFG=debug instead. Feel free to add -j8 or similar to build faster.
  • If the build succeeds, a file named wiz.exe should exist in the bin/ folder under the root of this repository.

Windows (Cygwin)

  • Install Cygwin
  • Within Cygwin, install either GCC or Clang, and install GNU Make.
  • Open a terminal window.
  • Run make in the terminal. For a debug target, run make CFG=debug instead. Feel free to add -j8 or similar to build faster.
  • If the build succeeds, a file named wiz.exe should exist in the bin/ folder under the root of this repository.
  • NOTE: This exe produced in this fashion will require Cygwin, so it is not preferred for native Windows executable compilation.

Mac OS X

  • Install Xcode and install Command Line Tools.
  • Open a terminal window.
  • Run make in the terminal. For a debug target, run make CFG=debug instead. Feel free to add -j8 or similar to build faster.
  • If the build succeeds, a file named wiz should exist in the bin/ folder under the root of this repository.

Linux

  • Install GCC or Clang.
  • Install GNU Make.
  • Run make in the terminal. For a debug target, run make CFG=debug instead. Feel free to add -j8 or similar to build faster.
  • If the build succeeds, a file named wiz should exist in the bin/ folder under the root of this repository.

Web / Emscripten

  • Install emscripten.
  • Install GNU Make.
  • Run make PLATFORM=emcc in the terminal. For a debug target, run make CFG=debug PLATFORM=emcc instead.
  • If the build succeeds, a file named wiz.js should exist in the bin/ folder under the root of this repository.
  • Copy the bin/wiz.js over try-in-browser/wiz.js to update the version used by the HTML try-in-browser sandbox.
  • See the try-in-browser/ test page for an example program that wraps the web version of the compiler.
  • Note that the license of JSNES (JavaScript Nintendo emulator) is GPL, and if distributed with the try-in-browser code, the other code must be released under the GPL. However, the non-emulator code in this folder is explicitly MIT, so remove this emulator dependency if these licensing terms are desired.

Installing

You don't need to install Wiz in order to use it. But if you want, you can install the compiler binary to your system path so that you can more easily use it. Wiz is currently only a single executable, and there are no standard libraries yet. It just has optional examples that are used by some test programs but they're not essential to running the compiler. Maybe down the road, this will change. In any case, here's some help.

Windows

  • First acquire wiz.exe for your platform or build it.
  • Create a folder for Wiz.
  • Copy wiz.exe into that folder.
  • Add that folder to your PATH environment variable.
  • Close and reopen any command window you had open.
  • You should be now able to execute wiz from any folder, not just the one that contains it.

Mac OS X, Linux

  • First acquire wiz for your platform or build it.
  • If you're using the makefile, you can use make install to install wiz to the system install location. (defaults to /usr/local/bin but can be customized via the DESTDIR and PREFIX variables)
  • Alternatively, copy the wiz executable into your preferred install folder (eg. /usr/local/bin or wherever else)

Language Quick Guide

This is a quick run-down of language features. This section is largely unfinished, so please consult example programs for better documentation!

Banks

Banks are sections or segments that are used to hold the different pieces of the program, such as variables, constants, or executable code.

A bank declaration reserves a bank of memory with a given type and address. It has the following syntax:

bank name @ address, name @ address, name @ address : [type; size];

The type of bank of bank determines what kind of declarations can be placed there.

  • vardata is uninitialized RAM. useful for variables that can be written to and read back at runtime. Because this section is uninitialized, variables declared here must be assigned a value at execution time. Because of these limitations, compiled code and and constant data cannot be placed here.
  • prgdata is ROM used for program code and constants. Cannot be written to.
  • constdata is ROM used for constants and program code. Cannot be written to.
  • chrdata is ROM used for character / tile graphic data. This type of bank has a special meaning on platforms like the NES, where character data is on a separate memory bus. It is otherwise the same as constdata.
  • The distinction between prgdata and constdata will only exist on Harvard architectures, where the program and constant data live on separate buses. Otherwise, just use it for clarifying the purpose of ROM banks, if it matters.
  • varinitdata is initialized RAM. Useful for programs with code and data that get uploaded to RAM.

The size of a bank is the number of bytes that it will hold. Exceeding this size limitation is considered an error.

The address of a bank after the @ is optional but if it appears, it must be an integer literal. Without an address, the bank can cannot contain label declarations.

After a bank is declared, it must be selected to be used by an in directive.

in bank {
    // ...
}

An address can also be provided to an in directive. If a bank previously had no address, then the address becomes the origin of the bank. However, if a bank previously had an address, the block is seeked ahead for to given address.

in bank @ address {
    // ...
}

Example:

bank zeropage @ 0x00 : [vardata; 256];

in zeropage {
    var timer : u8;
}

Constants

A constant declaration reserves pre-initialized data that can't be written to at run-time. A constant can appear in a ROM bank, such as prgdata, constdata, or chrdata.

const name : type = value;

Example:

const hp : u8 = 100;
const wow : [u8] = [0, 1, 2, 3, 4, 5, 6];
const egg : [u16; 5] = [12345, 54321, 32333, 49293];

If the type is provided, the initializer is narrowed to the type provided. This means that integer initializers of type iexpr that have no size can be implicitly narrowed to sized integer types like u8, and array initializers of type [iexpr] can be narrowed to [u8], and so on.

Note that The size of an array type can be left off, if the tyep.

If the type of a constant can be inferred from the value, then the type can be left off. However, only types with a known size can be constants. iexpr has unknown size, so type-suffixes on integers may be required.

Example:

const length = 100u8;
const message = "HELLO WORLD";
const table = embed "table.bin";

The name of a constant is also optional, when placing data in the ROM is desired but a label to it isn't needed.

Example:

const = embed "hero.chr";

Array comprehensions are a nice way to generate tables of data.

const tripled_values : [u8] = [x * 3 for let x in 0 .. 49];

Variables

A variable declaration reserves a space for mutable storage. A variable can appear either in RAM bank such as vardata, or varinitdata.

var x : u8;

A variable declared in a varinitdata bank is allowed to have a initializer.

var x = 100;

If an initializer is given, the type can be omitted if the initializer expression has a type of known size.

var x =
var message = "hello";

A variable declared in a vardata bank, on the other hand, is reserved in uninitialized RAM, so it cannot have an initializer and can only be assigned a value at run-time.

Extern Variables (Memory Mapped I/O Registers)

An extern variable can be used to declare a piece of external storage that exists at a specific address. Its primary use is for defining memory-mapped I/O registers on a system, so that they can referenced by name.

Example:

extern var reg @ 0xF000; // read/write
extern writeonly reg2 @ 0xF001; // write-only
extern const reg3 @ 0xF002; // read-only

Let Definitions

A let definition can be used to bind a name to a constant expression. This can reduce the amount of magic numbers appearing in code. These don't require any ROM space where they're defined, but are instead are substituted into whatever expressions that use them.

let name = expression;

The expression of a let definition can be any valid expression. It is recommended to not pass expressions that contain side-effects. In the future, the compiler may forbid this, but for now this is not validated.

Example:

let HP = 100;
let NAME = "Hello";

It is also for a let definition to take arguments:

let CEILING_DIV(x, y) = (x + y - 1) / y;
let MAP_WIDTH = CEILING_DIV(1000, 8);

Empty Statement

A single ; is allowed anywhere, and is an empty statement. It has no effect.

Assignment Statement

An assignment statement is used to store a value somewhere. Depending on where the value is stored, it can be retrieved again later.

dest = source; // assignment
dest += source; // compound assignment
dest++; // post-increment
dest--; // post-decrement
++dest; // pre-increment
--dest; // pre-decrement

The left-hand side of an assignment = is the destination, which can be a mutable register or a mutable location in memory. The right-hand side of an assignment = is the source, which can be a constant, a register, a readable memory location, or a run-time calculation. The source and destination must be of compatible type, or it is an error.

Example:

x = 100;
a = max_hp - hp;
damage = a;

After constant expression folding, any remaining run-time operations must have instructions available for them. It must be either an exact instruction that does the calcuation and stores it in the destination, or a series of instructions that first load the into the destination, and then modify the destination. For an assignment containing a binary expression like a = b + c;, instruction selection will first look for an exact instruction of form a = b + c;. Failing that, it will attempt decomposing the assignment a = b + c into the form a = b; a = a + c;.

Failing that, it is an error, and will probably require the assignment to be manually re-written in terms of multiple assignment statements. Assignments containing run-time operations cannot require an implicit temporary expression, or it is an error.

Most expressions are evaluated in left-to-right fashion, and as a result, they must be written so that they can be left-folded without temporaries. This requirement means parenthesized expressions can happen on the left, but the right-hand side will be a constant or simple term.

Example:

// This is OK, because it can be decomposed.
a = (5 + c) - b;
// The above becomes:
// a = 5;
// a = a + c
// a = a - b;

// This is an error, even in its reduced form:
a = b - (5 + c);
// The above becomes:
// a = b;
// a = a - (5 + c) // the (5 + c) requires a temporary.

Oftentimes, it won't be possible to assign a value directly to memory. In these cases, the code will need to put an intermediate assignment into a register first, and then store the register into the memory afterwards.

Example:

// If there's no instruction available to assign immediate values to memory, this line will be an error.
hp = 100;

// It must be re-written to use a register in the middle.
// This will work if there is a register named a,
// and there is an instruction to load an immediate to register a,
// and there is another instruction to load memory with the value of the register.
a = 100;
hp = a;

Assignments can be chained together if they're compatible.

Example:

// This is the same as:
// a = x;
// y = a;
y = a = x;

// This is the same as:
// a = hp;
// a = a - 10;
// hp = a;
hp = a = hp - 10;

Compound assignment operators exist for most binary arithmetic operations. For some operation +, the statement a += b is the same as writing a = a + b;. If the right-hand side contains a calculation it is implicitly parenthesized, so that a += b + c is the same as a = a + (b + c);

Example:

a += 5; // same as a = a + 5;

There are also unary incrementation ++ and decrementation -- operators.

Example:

x++;
y--;
++x;
++y;

Sometimes assignments might be used for their side-effects only, such as when writing to an external hardware register. Some such registers are writeonly, which indicates that they cannot be read back later, or that reading back will produce an open-bus value.

snes.ppu.bg_mode = a;

Similarly, there can be side-effects that occur when reading an external hardware register.

Assignments may also result in processor status flag registers being changed as a side-effect. Sometimes, a throw-away calculation in some registers or temporary memory might be performed simply to affect a conditional flag. The effect on flags depends on what sequence of instructions are generated as a result of the assignment. Not every operation affects every flag consistently, due to the design of the processor hardware. Knowing what instructions affect flags and which don't requires some study.

A common-to-find flag is the zero flag. If an instruction affects the zero flag, it might be used to indicate whether the result of the last operation was equal to zero. This side-effect can be used sometimes to avoid an extra comparison instruction later.

Example:

x = 10;
do {
    x--;
} while !zero;

A carry flag is a common flag to be affected by arithmetic operations such as addition/subtraction. The interpretation of a carry flag is dependent on the system. There are operations like add with carry +#, subtract with carry -#, rotate left with carry <<<<#, and rotate right with carry >>>># which depend upon the last state of the carry. These operations can be useful for extending arithmetic operations to work on larger numeric values.

Example:

// add 0x0134 to bc.
c = a = c + 0x34;
b = a = b +# 0x01;

Some CPUs allow the carry to be set or cleared by assigning to it.

Examples:

carry = false;
carry = true;

Call Statement

A call statement is a kind of statement that invokes a function. If the function being called produces a return value, the value is discarded. If the function call returns, the code following it will execute. Intrinsic instructions can also be called in the same manner.

function(argument1, argument2, argument3);

Block Statement

A block statement is a scoped section of code that contains a collection of statements. It is started with an opening brace { and must be terminated later with a closing brace }. All declarations within.

Many types of statement contain block statements as part of their syntax. For these kinds of statements, the braces delimiting a block statement are not optional, even if only a single statement is present.

If Statement

An if statement allows a block of code to be executed conditionally. The condition of an if statement is some expression of type bool. If a condition evaluates to true, then the block directly following it is executed. Otherwise, the block immediately following the condition is skipped, and the next alternative delimited by an else if is evaluated. Finally, there is else when all other alternatives have evaluated to false.

if condition {
    statements;
} else if condition {
    statements;
} else {
    statements;
}

A conditional expression can be a register term of type bool or its negation, or some combination of multiple bool operands with && and || operators

Example:

a = hp;
if zero {
    dead();
}

There are also high-level operators for comparison: ==, !=, <, >=, >, <=. Which kinds of comparisons are possible, and which registers can perform comparisons, depends on the target system.

Example:

a = gold;
x = item_index;

if a >= item_cost[x] {
    a -= item_cost[x];
    gold = a;
    buy_item(x);
} else {
    not_enough();
}

A conditional statement can also use "side-effect expressions" that evaluate some operation as required before a conditional expression. This sort of "setup-and-test" conditional can make some code read better, used carefully.

Example:

if { a = attack - defense; } && (carry || zero) {
    block_attack();
}

It is possible to use bit-indexing $ to test a single-bit of a value and get its boolean result. Which registers are capable of this, as well as the bits that can be selected, depends on the target system.

Example:

if b$7 {
    vertical_flip();
} else if b$6 {
    horizontal_flip();
}

While Statement

A while statement allows a block of code to be executed conditionally 0 or more times. The block is repeated as long as the condition provided still evaluates to true.

while condition {
    statement;
    statement;
    statement;
}

Example:

x = count;
while !zero {
    beep();
    x--;
}

Do/While Statement

A do ... while statement allows a block of code to be executed unconditionally once, followed by conditional execution 0 or more times. The block is entered once, and will repeat more times as long as the condition provided still evaluates to true.

do {
    statement;
    statement;
    statement;
} while condition;

Note that do ... while statements are slightly more efficient than while statements, because they fall-through to the next code once the condition is false, and only jump to the top of the loop if the condition is still true.

For Statement

A for statement allows iteration over a sequence of values, and executes a block of code for each step through the sequence.

for iterator in iterable {
    statement;
    statement;
    statement;
}

Currently, for loops only support iteration on comple-time integer ranges.

for counter in start .. end {
    statement;
    statement;
    statement;
}

for counter in start .. end by step {
    statement;
    statement;
    statement;
}

Example:

for x in 0 .. 31 {
    beep();
}

start .. end is an inclusive range. If the loop terminates normally, then after the loop, the counter provided will have the first value that is outside of the sequence.

Example:

// x is a u8 register that can be incremented and decremented.

// x will now be 32.
for x in 0 .. 31 {
    // ...
}

// x will be 0 after the loop.
for x in 31 .. 0 by -1 {
    // ...
}

// x will be 0 after the loop.
for x in 0 .. 255 {
    // ...
}

Return Statements

A return statement is used to return from the currently executing function.

return;
return if condition;

If used in a function that has no return type, it cannot have a return value. If used in a function that has a return type, the return value must be of the same type.

Example:

func collect_egg() {
    a = egg_count;
    if a >= 5 {
        return;
    }

    egg_count++;
}

func triple(value : u8 in b) : u8 in a {
    return b + b + b;
}

A return that exists outside of any function body is a low-level return instruction. This would pop a location from the stack and jump there.

Tail-Call Statements

A statement of form return f(); is a tail-call statement. It gets subsituted for a goto, and avoids the overhead of subroutine call that would push a program counter to the stack. However, unlike a goto, a tail-call will evaluate arguments to a function, like a normal function call.

func call_subroutine(dest : u16 in hl) {
    goto *(dest as *func);
}

Goto Statements

A goto is a low-level form of branching which jumps to another part of the program. Most high-level control structures are internally implemented in terms of goto.

goto destination;
goto destination if condition;

The destination of a goto can be a label, a function, or a function pointer expression.

Example:

x--; goto there if zero;

loop:
    goto loop;

there:

Code that uses goto should ensure that everything is already set up correctly before branching, because goto does not pass any arguments to its destination. If a goto that passes arguments to a function is required, use a tail-call statement of form return f(arg, arg, arg) instead.

Break and Continue

Loop statements such as while, do ... while, for have two forms of branching statements in their blocks: break and continue.

break;
continue;
break if condition;
continue if condition;

A break statement will terminate a loop early, and jump ahead to the code that immediately follows a loop's block.

A continue statement will skip to the next iteration of the loop. In for loops, this will skip to the code that performs an increment and branch. In while and do ... while loops, this will skip to the conditional branch of the loop.

Functions and Labels

A function is a piece of code that performs a specific set of actions and operations. They can be executed by other parts of the program, and can take arguments and produce a return value.


鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
热门推荐
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap