Last active
March 11, 2023 10:37
-
-
Save Tetralux/51be6106bc915728ced10504d6fec7a8 to your computer and use it in GitHub Desktop.
A simple example of a calculator programming language that compiles to native code!
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// A simple example of a calculator programming language, that compiles to native code! | |
// | |
// Written by Tetralux <[email protected]>, 2023-03-09. | |
// | |
// Programs are a string that you pass as an argument in the form of a mathmetical expression. | |
// e.g: '10 + 4 - 1 + 7'. | |
// This program will generate some Zig code that computes the answer, and prints it to stdout. | |
// It then will invoke the Zig compiler as a subprocess to compile and run this program. | |
// | |
const std = @import("std"); | |
const TokenKind = enum { | |
none, | |
plus, | |
minus, | |
number, | |
}; | |
const Token = struct { | |
kind: TokenKind, | |
str: []const u8, | |
location: Loc, | |
}; | |
const Loc = struct { | |
line: u32, | |
column: u32, | |
length: u16, | |
}; | |
const Lexer = struct { | |
buf: []const u8, | |
current_location: Loc = .{ .line = 1, .column = 1, .length = 1 }, | |
fn eat(lex: *Lexer) !Token { | |
lex.skip_whitespace(); | |
if (lex.buf.len == 0) return error.EOF; | |
switch (lex.buf[0]) { | |
'0'...'9' => return lex.eat_number(), | |
'+' => return lex.eat_chars_as_token(1, .plus), | |
'-' => return lex.eat_chars_as_token(1, .minus), | |
else => return error.UnexpectedByte, | |
} | |
} | |
fn eat_chars_as_token(lex: *Lexer, num_bytes: usize, kind: TokenKind) !Token { | |
if (lex.buf.len < num_bytes) return error.EOF; | |
var loc = lex.current_location; | |
const str = lex.buf[0..num_bytes]; | |
loc.length = @intCast(u16, num_bytes); | |
lex.buf = lex.buf[str.len..]; | |
lex.current_location.column += @intCast(u32, str.len); | |
return Token { | |
.kind = kind, | |
.str = str, | |
.location = loc, | |
}; | |
} | |
fn eat_number(lex: *Lexer) !Token { | |
var loc = lex.current_location; | |
var i: usize = 0; | |
while (i < lex.buf.len) : (i += 1) { | |
switch (lex.buf[i]) { | |
'0'...'9' => continue, | |
else => break, | |
} | |
} | |
if (i == 0) return error.BadNumber; | |
const str = lex.buf[0..i]; | |
loc.length = @intCast(u16, str.len); | |
lex.buf = lex.buf[i..]; | |
lex.current_location.column += @intCast(u32, str.len); | |
return Token { | |
.kind = .number, | |
.str = str, | |
.location = loc, | |
}; | |
} | |
fn skip_whitespace(lex: *Lexer) void { | |
if (lex.buf.len == 0) return; | |
var i: usize = 0; | |
while (i < lex.buf.len) : (i += 1) { | |
switch (lex.buf.ptr[i]) { | |
' ', '\t' => lex.current_location.column += 1, | |
'\n' => { | |
lex.current_location.line += 1; | |
lex.current_location.column = 1; | |
}, | |
else => break, | |
} | |
} | |
lex.buf = lex.buf[i..]; | |
} | |
fn expect(lex: *Lexer, kind: TokenKind) !Token { | |
const token = try lex.eat(); | |
if (token.kind == kind) return token; | |
return error.UnexpectedToken; | |
} | |
}; | |
pub fn main() !void { | |
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); | |
const ally = arena.allocator(); | |
const args = try std.process.argsAlloc(ally); | |
if (args.len == 1) { | |
std.debug.print("error: no program text specified.\n", .{}); | |
std.debug.print("e.g: {s} 10 + 4 - 1 + 7\n", .{ args[0] }); | |
std.os.exit(1); | |
} | |
const input = try std.mem.join(ally, " ", args[1..]); | |
var out = try std.ArrayList(u8).initCapacity(ally, 4096); | |
var err_loc: Loc = undefined; | |
const program = program_from_string(&out, input, &err_loc) catch |err| { | |
const w = std.io.getStdErr().writer(); | |
try w.print("syntax error: {s}\n", .{ @errorName(err) }); | |
try show_context(w, input, err_loc); | |
std.os.exit(2); | |
}; | |
// std.debug.print("{s}\n", .{ program }); | |
try std.fs.cwd().writeFile("tiny_output.zig", program); | |
// defer std.fs.cwd().deleteFile("tiny_output.zig") catch {}; | |
var exe = std.ChildProcess.init(&.{"zig", "run", "tiny_output.zig"}, ally); | |
_ = try exe.spawnAndWait(); | |
} | |
fn program_from_string(out: *std.ArrayList(u8), input: []const u8, err_loc: *Loc) ![]const u8 { | |
try out.appendSlice( | |
\\const std = @import("std"); | |
\\ | |
\\pub fn main() !void { | |
\\ var result: i64 = 0; | |
\\ | |
); | |
var lex = Lexer { | |
.buf = input, | |
}; | |
errdefer err_loc.* = lex.current_location; | |
const initial = try lex.expect(.number); | |
try out.writer().print(" result = {s};\n", .{ initial.str }); | |
while (true) { | |
const operator = lex.eat() catch |e| switch (e) { | |
error.EOF => break, | |
else => return e, | |
}; | |
const value = try lex.expect(.number); | |
try out.writer().print(" result = result {s} {s};\n", .{ operator.str, value.str }); | |
} | |
try out.appendSlice( | |
\\ | |
\\ std.io.getStdOut().writer().print("{}\n", .{ result }) catch |err| { | |
\\ std.debug.print("error: unable to write to stdout: {s}\n", .{ @errorName(err) }); | |
\\ }; | |
\\ | |
); | |
try out.appendSlice("}"); | |
const zig_string = try out.toOwnedSlice(); | |
return zig_string; | |
} | |
fn show_context(w: anytype, input: []const u8, err_loc: Loc) !void { | |
try w.writeAll(" | "); | |
try w.writeAll(input); | |
try w.writeAll("\n |"); | |
for (0..err_loc.column) |_| try w.writeAll("-"); | |
try w.writeAll("^"); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment